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