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