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