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