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