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 bool write(const SegmentedString&, bool appendData);
61 virtual void finish();
62
isWaitingForScripts() const63 virtual bool isWaitingForScripts() const { return false; }
64
checkBuffer(int len=10)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
FTPDirectoryTokenizer(HTMLDocument * doc)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
appendEntry(const String & filename,const String & size,const String & date,bool isDirectory)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->createElementNS(xhtmlNamespaceURI, "td", ec);
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->createElementNS(xhtmlNamespaceURI, "td", ec);
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->createElementNS(xhtmlNamespaceURI, "td", ec);
138 element->appendChild(new Text(m_doc, size), ec);
139 element->setAttribute("class", "ftpDirectoryFileSize", ec);
140 rowElement->appendChild(element, ec);
141 }
142
createTDForFilename(const String & filename)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->createElementNS(xhtmlNamespaceURI, "a", ec);
154 anchorElement->setAttribute("href", fullURL, ec);
155 anchorElement->appendChild(new Text(m_doc, filename), ec);
156
157 RefPtr<Element> tdElement = m_doc->createElementNS(xhtmlNamespaceURI, "td", ec);
158 tdElement->appendChild(anchorElement, ec);
159
160 return tdElement.release();
161 }
162
processFilesizeString(const String & size,bool isDirectory)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
wasLastDayOfMonth(int year,int month,int day)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 */
gmtimeQt(const QDateTime & input)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
localTimeQt(const time_t * const timep,struct tm * result)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 #define localtime_r(x, y) localtime_s((y), (x))
239 #endif
240
processFileDateString(const FTPTime & fileTime)241 static String processFileDateString(const FTPTime& fileTime)
242 {
243 // FIXME: Need to localize this string?
244
245 String timeOfDay;
246
247 if (!(fileTime.tm_hour == 0 && fileTime.tm_min == 0 && fileTime.tm_sec == 0)) {
248 int hour = fileTime.tm_hour;
249 ASSERT(hour >= 0 && hour < 24);
250
251 if (hour < 12) {
252 if (hour == 0)
253 hour = 12;
254 timeOfDay = String::format(", %i:%02i AM", hour, fileTime.tm_min);
255 } else {
256 hour = hour - 12;
257 if (hour == 0)
258 hour = 12;
259 timeOfDay = String::format(", %i:%02i PM", hour, fileTime.tm_min);
260 }
261 }
262
263 // If it was today or yesterday, lets just do that - but we have to compare to the current time
264 struct tm now;
265 time_t now_t = time(NULL);
266 localtime_r(&now_t, &now);
267
268 // localtime does "year = current year - 1900", compensate for that for readability and comparison purposes
269 now.tm_year += 1900;
270
271 if (fileTime.tm_year == now.tm_year) {
272 if (fileTime.tm_mon == now.tm_mon) {
273 if (fileTime.tm_mday == now.tm_mday)
274 return "Today" + timeOfDay;
275 if (fileTime.tm_mday == now.tm_mday - 1)
276 return "Yesterday" + timeOfDay;
277 }
278
279 if (now.tm_mday == 1 && (now.tm_mon == fileTime.tm_mon + 1 || now.tm_mon == 0 && fileTime.tm_mon == 11) &&
280 wasLastDayOfMonth(fileTime.tm_year, fileTime.tm_mon, fileTime.tm_mday))
281 return "Yesterday" + timeOfDay;
282 }
283
284 if (fileTime.tm_year == now.tm_year - 1 && fileTime.tm_mon == 12 && fileTime.tm_mday == 31 && now.tm_mon == 1 && now.tm_mday == 1)
285 return "Yesterday" + timeOfDay;
286
287 static const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "???" };
288
289 int month = fileTime.tm_mon;
290 if (month < 0 || month > 11)
291 month = 12;
292
293 String dateString;
294
295 if (fileTime.tm_year > -1)
296 dateString = String::format("%s %i, %i", months[month], fileTime.tm_mday, fileTime.tm_year);
297 else
298 dateString = String::format("%s %i, %i", months[month], fileTime.tm_mday, now.tm_year);
299
300 return dateString + timeOfDay;
301 }
302
parseAndAppendOneLine(const String & inputLine)303 void FTPDirectoryTokenizer::parseAndAppendOneLine(const String& inputLine)
304 {
305 ListResult result;
306
307 FTPEntryType typeResult = parseOneFTPLine(inputLine.latin1().data(), m_listState, result);
308
309 // FTPMiscEntry is a comment or usage statistic which we don't care about, and junk is invalid data - bail in these 2 cases
310 if (typeResult == FTPMiscEntry || typeResult == FTPJunkEntry)
311 return;
312
313 String filename(result.filename, result.filenameLength);
314 if (result.type == FTPDirectoryEntry) {
315 filename.append("/");
316
317 // We have no interest in linking to "current directory"
318 if (filename == "./")
319 return;
320 }
321
322 LOG(FTP, "Appending entry - %s, %s", filename.ascii().data(), result.fileSize.ascii().data());
323
324 appendEntry(filename, processFilesizeString(result.fileSize, result.type == FTPDirectoryEntry), processFileDateString(result.modifiedTime), result.type == FTPDirectoryEntry);
325 }
326
createTemplateDocumentData(Settings * settings)327 static inline PassRefPtr<SharedBuffer> createTemplateDocumentData(Settings* settings)
328 {
329 RefPtr<SharedBuffer> buffer = 0;
330 if (settings)
331 buffer = SharedBuffer::createWithContentsOfFile(settings->ftpDirectoryTemplatePath());
332 if (buffer)
333 LOG(FTP, "Loaded FTPDirectoryTemplate of length %i\n", buffer->size());
334 return buffer.release();
335 }
336
loadDocumentTemplate()337 bool FTPDirectoryTokenizer::loadDocumentTemplate()
338 {
339 DEFINE_STATIC_LOCAL(RefPtr<SharedBuffer>, templateDocumentData, (createTemplateDocumentData(m_doc->settings())));
340 // FIXME: Instead of storing the data, we'd rather actually parse the template data into the template Document once,
341 // store that document, then "copy" it whenever we get an FTP directory listing. There are complexities with this
342 // approach that make it worth putting this off.
343
344 if (!templateDocumentData) {
345 LOG_ERROR("Could not load templateData");
346 return false;
347 }
348
349 // Tokenize the template as an HTML document synchronously
350 setForceSynchronous(true);
351 HTMLTokenizer::write(String(templateDocumentData->data(), templateDocumentData->size()), true);
352 setForceSynchronous(false);
353
354 RefPtr<Element> tableElement = m_doc->getElementById("ftpDirectoryTable");
355 if (!tableElement)
356 LOG_ERROR("Unable to find element by id \"ftpDirectoryTable\" in the template document.");
357 else if (!tableElement->hasTagName(tableTag))
358 LOG_ERROR("Element of id \"ftpDirectoryTable\" is not a table element");
359 else
360 m_tableElement = static_cast<HTMLTableElement*>(tableElement.get());
361
362 // Bail if we found the table element
363 if (m_tableElement)
364 return true;
365
366 // Otherwise create one manually
367 ExceptionCode ec;
368 tableElement = m_doc->createElementNS(xhtmlNamespaceURI, "table", ec);
369 m_tableElement = static_cast<HTMLTableElement*>(tableElement.get());
370 m_tableElement->setAttribute("id", "ftpDirectoryTable", ec);
371
372 // If we didn't find the table element, lets try to append our own to the body
373 // If that fails for some reason, cram it on the end of the document as a last
374 // ditch effort
375 if (Element* body = m_doc->body())
376 body->appendChild(m_tableElement, ec);
377 else
378 m_doc->appendChild(m_tableElement, ec);
379
380 return true;
381 }
382
createBasicDocument()383 void FTPDirectoryTokenizer::createBasicDocument()
384 {
385 LOG(FTP, "Creating a basic FTP document structure as no template was loaded");
386
387 // FIXME: Make this "basic document" more acceptable
388
389 ExceptionCode ec;
390
391 RefPtr<Element> bodyElement = m_doc->createElementNS(xhtmlNamespaceURI, "body", ec);
392
393 m_doc->appendChild(bodyElement, ec);
394
395 RefPtr<Element> tableElement = m_doc->createElementNS(xhtmlNamespaceURI, "table", ec);
396 m_tableElement = static_cast<HTMLTableElement*>(tableElement.get());
397 m_tableElement->setAttribute("id", "ftpDirectoryTable", ec);
398
399 bodyElement->appendChild(m_tableElement, ec);
400 }
401
write(const SegmentedString & s,bool)402 bool FTPDirectoryTokenizer::write(const SegmentedString& s, bool /*appendData*/)
403 {
404 // Make sure we have the table element to append to by loading the template set in the pref, or
405 // creating a very basic document with the appropriate table
406 if (!m_tableElement) {
407 if (!loadDocumentTemplate())
408 createBasicDocument();
409 ASSERT(m_tableElement);
410 }
411
412 bool foundNewLine = false;
413
414 m_dest = m_buffer;
415 SegmentedString str = s;
416 while (!str.isEmpty()) {
417 UChar c = *str;
418
419 if (c == '\r') {
420 *m_dest++ = '\n';
421 foundNewLine = true;
422 // possibly skip an LF in the case of an CRLF sequence
423 m_skipLF = true;
424 } else if (c == '\n') {
425 if (!m_skipLF)
426 *m_dest++ = c;
427 else
428 m_skipLF = false;
429 } else {
430 *m_dest++ = c;
431 m_skipLF = false;
432 }
433
434 str.advance();
435
436 // Maybe enlarge the buffer
437 checkBuffer();
438 }
439
440 if (!foundNewLine) {
441 m_dest = m_buffer;
442 return false;
443 }
444
445 UChar* start = m_buffer;
446 UChar* cursor = start;
447
448 while (cursor < m_dest) {
449 if (*cursor == '\n') {
450 m_carryOver.append(String(start, cursor - start));
451 LOG(FTP, "%s", m_carryOver.ascii().data());
452 parseAndAppendOneLine(m_carryOver);
453 m_carryOver = String();
454
455 start = ++cursor;
456 } else
457 cursor++;
458 }
459
460 // Copy the partial line we have left to the carryover buffer
461 if (cursor - start > 1)
462 m_carryOver.append(String(start, cursor - start - 1));
463
464 return false;
465 }
466
finish()467 void FTPDirectoryTokenizer::finish()
468 {
469 // Possible the last line in the listing had no newline, so try to parse it now
470 if (!m_carryOver.isEmpty()) {
471 parseAndAppendOneLine(m_carryOver);
472 m_carryOver = String();
473 }
474
475 m_tableElement = 0;
476 fastFree(m_buffer);
477
478 HTMLTokenizer::finish();
479 }
480
FTPDirectoryDocument(Frame * frame)481 FTPDirectoryDocument::FTPDirectoryDocument(Frame* frame)
482 : HTMLDocument(frame)
483 {
484 #ifndef NDEBUG
485 LogFTP.state = WTFLogChannelOn;
486 #endif
487 }
488
createTokenizer()489 Tokenizer* FTPDirectoryDocument::createTokenizer()
490 {
491 return new FTPDirectoryTokenizer(this);
492 }
493
494 }
495
496 #endif // ENABLE(FTPDIR)
497