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