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