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