Unreviewed, rolling out r234489.
[WebKit-https.git] / Source / WebCore / html / FTPDirectoryDocument.cpp
1 /*
2  * Copyright (C) 2007-2008, 2014-2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
20  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
23  */
24
25 #include "config.h"
26 #include "FTPDirectoryDocument.h"
27
28 #if ENABLE(FTPDIR)
29
30 #include "HTMLAnchorElement.h"
31 #include "HTMLBodyElement.h"
32 #include "HTMLDocumentParser.h"
33 #include "HTMLTableCellElement.h"
34 #include "HTMLTableElement.h"
35 #include "LocalizedStrings.h"
36 #include "Logging.h"
37 #include "FTPDirectoryParser.h"
38 #include "Settings.h"
39 #include "SharedBuffer.h"
40 #include "Text.h"
41 #include <wtf/GregorianDateTime.h>
42 #include <wtf/IsoMallocInlines.h>
43 #include <wtf/StdLibExtras.h>
44 #include <wtf/unicode/CharacterNames.h>
45
46 namespace WebCore {
47
48 WTF_MAKE_ISO_ALLOCATED_IMPL(FTPDirectoryDocument);
49
50 using namespace HTMLNames;
51     
52 class FTPDirectoryDocumentParser final : public HTMLDocumentParser {
53 public:
54     static Ref<FTPDirectoryDocumentParser> create(HTMLDocument& document)
55     {
56         return adoptRef(*new FTPDirectoryDocumentParser(document));
57     }
58
59 private:
60     void append(RefPtr<StringImpl>&&) override;
61     void finish() override;
62
63     // FIXME: Why do we need this?
64     bool isWaitingForScripts() const override { return false; }
65
66     void checkBuffer(int len = 10)
67     {
68         if ((m_dest - m_buffer) > m_size - len) {
69             // Enlarge buffer
70             int newSize = std::max(m_size * 2, m_size + len);
71             int oldOffset = m_dest - m_buffer;
72             m_buffer = static_cast<UChar*>(fastRealloc(m_buffer, newSize * sizeof(UChar)));
73             m_dest = m_buffer + oldOffset;
74             m_size = newSize;
75         }
76     }
77
78     FTPDirectoryDocumentParser(HTMLDocument&);
79
80     // The parser will attempt to load the document template specified via the preference
81     // Failing that, it will fall back and create the basic document which will have a minimal
82     // table for presenting the FTP directory in a useful manner
83     bool loadDocumentTemplate();
84     void createBasicDocument();
85
86     void parseAndAppendOneLine(const String&);
87     void appendEntry(const String& name, const String& size, const String& date, bool isDirectory);    
88     Ref<Element> createTDForFilename(const String&);
89
90     RefPtr<HTMLTableElement> m_tableElement;
91
92     bool m_skipLF { false };
93     
94     int m_size { 254 };
95     UChar* m_buffer;
96     UChar* m_dest;
97     String m_carryOver;
98     
99     ListState m_listState;
100 };
101
102 FTPDirectoryDocumentParser::FTPDirectoryDocumentParser(HTMLDocument& document)
103     : HTMLDocumentParser(document)
104     , m_buffer(static_cast<UChar*>(fastMalloc(sizeof(UChar) * m_size)))
105     , m_dest(m_buffer)
106 {
107 }
108
109 void FTPDirectoryDocumentParser::appendEntry(const String& filename, const String& size, const String& date, bool isDirectory)
110 {
111     auto& document = *this->document();
112
113     auto rowElement = m_tableElement->insertRow(-1).releaseReturnValue();
114     rowElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, AtomicString("ftpDirectoryEntryRow", AtomicString::ConstructFromLiteral));
115
116     auto typeElement = HTMLTableCellElement::create(tdTag, document);
117     typeElement->appendChild(Text::create(document, String(&noBreakSpace, 1)));
118     if (isDirectory)
119         typeElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, AtomicString("ftpDirectoryIcon ftpDirectoryTypeDirectory", AtomicString::ConstructFromLiteral));
120     else
121         typeElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, AtomicString("ftpDirectoryIcon ftpDirectoryTypeFile", AtomicString::ConstructFromLiteral));
122     rowElement->appendChild(typeElement);
123
124     auto nameElement = createTDForFilename(filename);
125     nameElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, AtomicString("ftpDirectoryFileName", AtomicString::ConstructFromLiteral));
126     rowElement->appendChild(nameElement);
127
128     auto dateElement = HTMLTableCellElement::create(tdTag, document);
129     dateElement->appendChild(Text::create(document, date));
130     dateElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, AtomicString("ftpDirectoryFileDate", AtomicString::ConstructFromLiteral));
131     rowElement->appendChild(dateElement);
132
133     auto sizeElement = HTMLTableCellElement::create(tdTag, document);
134     sizeElement->appendChild(Text::create(document, size));
135     sizeElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, AtomicString("ftpDirectoryFileSize", AtomicString::ConstructFromLiteral));
136     rowElement->appendChild(sizeElement);
137 }
138
139 Ref<Element> FTPDirectoryDocumentParser::createTDForFilename(const String& filename)
140 {
141     auto& document = *this->document();
142
143     String fullURL = document.baseURL().string();
144     if (fullURL.endsWith('/'))
145         fullURL = fullURL + filename;
146     else
147         fullURL = fullURL + '/' + filename;
148
149     auto anchorElement = HTMLAnchorElement::create(document);
150     anchorElement->setAttributeWithoutSynchronization(HTMLNames::hrefAttr, fullURL);
151     anchorElement->appendChild(Text::create(document, filename));
152
153     auto tdElement = HTMLTableCellElement::create(tdTag, document);
154     tdElement->appendChild(anchorElement);
155
156     return WTFMove(tdElement);
157 }
158
159 static String processFilesizeString(const String& size, bool isDirectory)
160 {
161     if (isDirectory)
162         return "--"_s;
163
164     bool valid;
165     int64_t bytes = size.toUInt64(&valid);
166     if (!valid)
167         return unknownFileSizeText();
168
169     if (bytes < 1000000)
170         return String::format("%.2f KB", static_cast<float>(bytes)/1000);
171
172     if (bytes < 1000000000)
173         return String::format("%.2f MB", static_cast<float>(bytes)/1000000);
174
175     return String::format("%.2f GB", static_cast<float>(bytes)/1000000000);
176 }
177
178 static bool wasLastDayOfMonth(int year, int month, int day)
179 {
180     static const int lastDays[] = { 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
181     if (month < 0 || month > 11)
182         return false;
183
184     if (month == 2) {
185         if (year % 4 == 0 && (year % 100 || year % 400 == 0)) {
186             if (day == 29)
187                 return true;
188             return false;
189         }
190
191         if (day == 28)
192             return true;
193         return false;
194     }
195
196     return lastDays[month] == day;
197 }
198
199 static String processFileDateString(const FTPTime& fileTime)
200 {
201     // FIXME: Need to localize this string?
202
203     String timeOfDay;
204
205     if (!(fileTime.tm_hour == 0 && fileTime.tm_min == 0 && fileTime.tm_sec == 0)) {
206         int hour = fileTime.tm_hour;
207         ASSERT(hour >= 0 && hour < 24);
208
209         if (hour < 12) {
210             if (hour == 0)
211                 hour = 12;
212             timeOfDay = String::format(", %i:%02i AM", hour, fileTime.tm_min);
213         } else {
214             hour = hour - 12;
215             if (hour == 0)
216                 hour = 12;
217             timeOfDay = String::format(", %i:%02i PM", hour, fileTime.tm_min);
218         }
219     }
220
221     // If it was today or yesterday, lets just do that - but we have to compare to the current time
222     GregorianDateTime now;
223     now.setToCurrentLocalTime();
224
225     if (fileTime.tm_year == now.year()) {
226         if (fileTime.tm_mon == now.month()) {
227             if (fileTime.tm_mday == now.monthDay())
228                 return "Today" + timeOfDay;
229             if (fileTime.tm_mday == now.monthDay() - 1)
230                 return "Yesterday" + timeOfDay;
231         }
232         
233         if (now.monthDay() == 1 && (now.month() == fileTime.tm_mon + 1 || (now.month() == 0 && fileTime.tm_mon == 11)) &&
234             wasLastDayOfMonth(fileTime.tm_year, fileTime.tm_mon, fileTime.tm_mday))
235                 return "Yesterday" + timeOfDay;
236     }
237
238     if (fileTime.tm_year == now.year() - 1 && fileTime.tm_mon == 12 && fileTime.tm_mday == 31 && now.month() == 1 && now.monthDay() == 1)
239         return "Yesterday" + timeOfDay;
240
241     static const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "???" };
242
243     int month = fileTime.tm_mon;
244     if (month < 0 || month > 11)
245         month = 12;
246
247     String dateString;
248
249     if (fileTime.tm_year > -1)
250         dateString = makeString(months[month], ' ', String::number(fileTime.tm_mday), ", ", String::number(fileTime.tm_year));
251     else
252         dateString = makeString(months[month], ' ', String::number(fileTime.tm_mday), ", ", String::number(now.year()));
253
254     return dateString + timeOfDay;
255 }
256
257 void FTPDirectoryDocumentParser::parseAndAppendOneLine(const String& inputLine)
258 {
259     ListResult result;
260     CString latin1Input = inputLine.latin1();
261
262     FTPEntryType typeResult = parseOneFTPLine(latin1Input.data(), m_listState, result);
263
264     // FTPMiscEntry is a comment or usage statistic which we don't care about, and junk is invalid data - bail in these 2 cases
265     if (typeResult == FTPMiscEntry || typeResult == FTPJunkEntry)
266         return;
267
268     String filename(result.filename, result.filenameLength);
269     if (result.type == FTPDirectoryEntry) {
270         filename.append('/');
271
272         // We have no interest in linking to "current directory"
273         if (filename == "./")
274             return;
275     }
276
277     LOG(FTP, "Appending entry - %s, %s", filename.ascii().data(), result.fileSize.ascii().data());
278
279     appendEntry(filename, processFilesizeString(result.fileSize, result.type == FTPDirectoryEntry), processFileDateString(result.modifiedTime), result.type == FTPDirectoryEntry);
280 }
281
282 static inline RefPtr<SharedBuffer> createTemplateDocumentData(const Settings& settings)
283 {
284     RefPtr<SharedBuffer> buffer = SharedBuffer::createWithContentsOfFile(settings.ftpDirectoryTemplatePath());
285     if (buffer)
286         LOG(FTP, "Loaded FTPDirectoryTemplate of length %zu\n", buffer->size());
287     return buffer;
288 }
289     
290 bool FTPDirectoryDocumentParser::loadDocumentTemplate()
291 {
292     static SharedBuffer* templateDocumentData = createTemplateDocumentData(document()->settings()).leakRef();
293     // FIXME: Instead of storing the data, it would be more efficient if we could parse the template data into the
294     // template Document once, store that document, then "copy" it whenever we get an FTP directory listing.
295     
296     if (!templateDocumentData) {
297         LOG_ERROR("Could not load templateData");
298         return false;
299     }
300
301     HTMLDocumentParser::insert(String(templateDocumentData->data(), templateDocumentData->size()));
302
303     auto& document = *this->document();
304
305     auto foundElement = makeRefPtr(document.getElementById(String("ftpDirectoryTable"_s)));
306     if (!foundElement)
307         LOG_ERROR("Unable to find element by id \"ftpDirectoryTable\" in the template document.");
308     else if (!is<HTMLTableElement>(foundElement))
309         LOG_ERROR("Element of id \"ftpDirectoryTable\" is not a table element");
310     else {
311         m_tableElement = downcast<HTMLTableElement>(foundElement.get());
312         return true;
313     }
314
315     m_tableElement = HTMLTableElement::create(document);
316     m_tableElement->setAttributeWithoutSynchronization(HTMLNames::idAttr, AtomicString("ftpDirectoryTable", AtomicString::ConstructFromLiteral));
317
318     // If we didn't find the table element, lets try to append our own to the body.
319     // If that fails for some reason, cram it on the end of the document as a last ditch effort.
320     if (auto body = makeRefPtr(document.bodyOrFrameset()))
321         body->appendChild(*m_tableElement);
322     else
323         document.appendChild(*m_tableElement);
324
325     return true;
326 }
327
328 void FTPDirectoryDocumentParser::createBasicDocument()
329 {
330     LOG(FTP, "Creating a basic FTP document structure as no template was loaded");
331
332     auto& document = *this->document();
333
334     auto bodyElement = HTMLBodyElement::create(document);
335     document.appendChild(bodyElement);
336
337     m_tableElement = HTMLTableElement::create(document);
338     m_tableElement->setAttributeWithoutSynchronization(HTMLNames::idAttr, AtomicString("ftpDirectoryTable", AtomicString::ConstructFromLiteral));
339     m_tableElement->setAttribute(HTMLNames::styleAttr, AtomicString("width:100%", AtomicString::ConstructFromLiteral));
340
341     bodyElement->appendChild(*m_tableElement);
342
343     document.processViewport("width=device-width", ViewportArguments::ViewportMeta);
344 }
345
346 void FTPDirectoryDocumentParser::append(RefPtr<StringImpl>&& inputSource)
347 {
348     // Make sure we have the table element to append to by loading the template set in the pref, or
349     // creating a very basic document with the appropriate table
350     if (!m_tableElement) {
351         if (!loadDocumentTemplate())
352             createBasicDocument();
353         ASSERT(m_tableElement);
354     }
355
356     bool foundNewLine = false;
357
358     m_dest = m_buffer;
359     SegmentedString string { String { WTFMove(inputSource) } };
360     while (!string.isEmpty()) {
361         UChar c = string.currentCharacter();
362
363         if (c == '\r') {
364             *m_dest++ = '\n';
365             foundNewLine = true;
366             // possibly skip an LF in the case of an CRLF sequence
367             m_skipLF = true;
368         } else if (c == '\n') {
369             if (!m_skipLF)
370                 *m_dest++ = c;
371             else
372                 m_skipLF = false;
373         } else {
374             *m_dest++ = c;
375             m_skipLF = false;
376         }
377
378         string.advance();
379
380         // Maybe enlarge the buffer
381         checkBuffer();
382     }
383
384     if (!foundNewLine) {
385         m_dest = m_buffer;
386         return;
387     }
388
389     UChar* start = m_buffer;
390     UChar* cursor = start;
391
392     while (cursor < m_dest) {
393         if (*cursor == '\n') {
394             m_carryOver.append(String(start, cursor - start));
395             LOG(FTP, "%s", m_carryOver.ascii().data());
396             parseAndAppendOneLine(m_carryOver);
397             m_carryOver = String();
398
399             start = ++cursor;
400         } else 
401             cursor++;
402     }
403
404     // Copy the partial line we have left to the carryover buffer
405     if (cursor - start > 1)
406         m_carryOver.append(String(start, cursor - start - 1));
407 }
408
409 void FTPDirectoryDocumentParser::finish()
410 {
411     // Possible the last line in the listing had no newline, so try to parse it now
412     if (!m_carryOver.isEmpty()) {
413         parseAndAppendOneLine(m_carryOver);
414         m_carryOver = String();
415     }
416
417     m_tableElement = nullptr;
418     fastFree(m_buffer);
419
420     HTMLDocumentParser::finish();
421 }
422
423 FTPDirectoryDocument::FTPDirectoryDocument(Frame* frame, const URL& url)
424     : HTMLDocument(frame, url)
425 {
426 #if !LOG_DISABLED
427     LogFTP.state = WTFLogChannelOn;
428 #endif
429 }
430
431 Ref<DocumentParser> FTPDirectoryDocument::createParser()
432 {
433     return FTPDirectoryDocumentParser::create(*this);
434 }
435
436 }
437
438 #endif // ENABLE(FTPDIR)