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