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
45using namespace std;
46
47namespace WebCore {
48
49using namespace HTMLNames;
50
51class FTPDirectoryDocumentParser : public HTMLDocumentParser {
52public:
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
61    virtual bool isWaitingForScripts() const { return false; }
62
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
75private:
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
101FTPDirectoryDocumentParser::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
111void 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
141PassRefPtr<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
161static 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
180static 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
201static 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
263void 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
288static 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
298bool 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
341void 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
359void 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
422void 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
436FTPDirectoryDocument::FTPDirectoryDocument(Frame* frame, const KURL& url)
437    : HTMLDocument(frame, url)
438{
439#ifndef NDEBUG
440    LogFTP.state = WTFLogChannelOn;
441#endif
442}
443
444PassRefPtr<DocumentParser> FTPDirectoryDocument::createParser()
445{
446    return FTPDirectoryDocumentParser::create(this);
447}
448
449}
450
451#endif // ENABLE(FTPDIR)
452