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