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