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