/* | |
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved. | |
* | |
* Redistribution and use in source and binary forms, with or without | |
* modification, are permitted provided that the following conditions | |
* are met: | |
* 1. Redistributions of source code must retain the above copyright | |
* notice, this list of conditions and the following disclaimer. | |
* 2. Redistributions in binary form must reproduce the above copyright | |
* notice, this list of conditions and the following disclaimer in the | |
* documentation and/or other materials provided with the distribution. | |
* | |
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY | |
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR | |
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
*/ | |
#include "config.h" | |
#if ENABLE(FTPDIR) | |
#include "FTPDirectoryDocument.h" | |
#include "CharacterNames.h" | |
#include "CString.h" | |
#include "HTMLNames.h" | |
#include "HTMLTableElement.h" | |
#include "HTMLTokenizer.h" | |
#include "LocalizedStrings.h" | |
#include "Logging.h" | |
#include "FTPDirectoryParser.h" | |
#include "SegmentedString.h" | |
#include "Settings.h" | |
#include "SharedBuffer.h" | |
#include "Text.h" | |
#include <wtf/CurrentTime.h> | |
#include <wtf/StdLibExtras.h> | |
using namespace std; | |
namespace WebCore { | |
using namespace HTMLNames; | |
class FTPDirectoryTokenizer : public HTMLTokenizer { | |
public: | |
FTPDirectoryTokenizer(HTMLDocument*); | |
virtual void write(const SegmentedString&, bool appendData); | |
virtual void finish(); | |
virtual bool isWaitingForScripts() const { return false; } | |
inline void checkBuffer(int len = 10) | |
{ | |
if ((m_dest - m_buffer) > m_size - len) { | |
// Enlarge buffer | |
int newSize = max(m_size * 2, m_size + len); | |
int oldOffset = m_dest - m_buffer; | |
m_buffer = static_cast<UChar*>(fastRealloc(m_buffer, newSize * sizeof(UChar))); | |
m_dest = m_buffer + oldOffset; | |
m_size = newSize; | |
} | |
} | |
private: | |
// The tokenizer will attempt to load the document template specified via the preference | |
// Failing that, it will fall back and create the basic document which will have a minimal | |
// table for presenting the FTP directory in a useful manner | |
bool loadDocumentTemplate(); | |
void createBasicDocument(); | |
void parseAndAppendOneLine(const String&); | |
void appendEntry(const String& name, const String& size, const String& date, bool isDirectory); | |
PassRefPtr<Element> createTDForFilename(const String&); | |
Document* m_doc; | |
RefPtr<HTMLTableElement> m_tableElement; | |
bool m_skipLF; | |
bool m_parsedTemplate; | |
int m_size; | |
UChar* m_buffer; | |
UChar* m_dest; | |
String m_carryOver; | |
ListState m_listState; | |
}; | |
FTPDirectoryTokenizer::FTPDirectoryTokenizer(HTMLDocument* doc) | |
: HTMLTokenizer(doc, false) | |
, m_doc(doc) | |
, m_skipLF(false) | |
, m_parsedTemplate(false) | |
, m_size(254) | |
, m_buffer(static_cast<UChar*>(fastMalloc(sizeof(UChar) * m_size))) | |
, m_dest(m_buffer) | |
{ | |
} | |
void FTPDirectoryTokenizer::appendEntry(const String& filename, const String& size, const String& date, bool isDirectory) | |
{ | |
ExceptionCode ec; | |
RefPtr<Element> rowElement = m_tableElement->insertRow(-1, ec); | |
rowElement->setAttribute("class", "ftpDirectoryEntryRow", ec); | |
RefPtr<Element> element = m_doc->createElement(tdTag, false); | |
element->appendChild(Text::create(m_doc, String(&noBreakSpace, 1)), ec); | |
if (isDirectory) | |
element->setAttribute("class", "ftpDirectoryIcon ftpDirectoryTypeDirectory", ec); | |
else | |
element->setAttribute("class", "ftpDirectoryIcon ftpDirectoryTypeFile", ec); | |
rowElement->appendChild(element, ec); | |
element = createTDForFilename(filename); | |
element->setAttribute("class", "ftpDirectoryFileName", ec); | |
rowElement->appendChild(element, ec); | |
element = m_doc->createElement(tdTag, false); | |
element->appendChild(Text::create(m_doc, date), ec); | |
element->setAttribute("class", "ftpDirectoryFileDate", ec); | |
rowElement->appendChild(element, ec); | |
element = m_doc->createElement(tdTag, false); | |
element->appendChild(Text::create(m_doc, size), ec); | |
element->setAttribute("class", "ftpDirectoryFileSize", ec); | |
rowElement->appendChild(element, ec); | |
} | |
PassRefPtr<Element> FTPDirectoryTokenizer::createTDForFilename(const String& filename) | |
{ | |
ExceptionCode ec; | |
String fullURL = m_doc->baseURL().string(); | |
if (fullURL[fullURL.length() - 1] == '/') | |
fullURL.append(filename); | |
else | |
fullURL.append("/" + filename); | |
RefPtr<Element> anchorElement = m_doc->createElement(aTag, false); | |
anchorElement->setAttribute("href", fullURL, ec); | |
anchorElement->appendChild(Text::create(m_doc, filename), ec); | |
RefPtr<Element> tdElement = m_doc->createElement(tdTag, false); | |
tdElement->appendChild(anchorElement, ec); | |
return tdElement.release(); | |
} | |
static String processFilesizeString(const String& size, bool isDirectory) | |
{ | |
if (isDirectory) | |
return "--"; | |
bool valid; | |
int64_t bytes = size.toUInt64(&valid); | |
if (!valid) | |
return unknownFileSizeText(); | |
if (bytes < 1000000) | |
return String::format("%.2f KB", static_cast<float>(bytes)/1000); | |
if (bytes < 1000000000) | |
return String::format("%.2f MB", static_cast<float>(bytes)/1000000); | |
return String::format("%.2f GB", static_cast<float>(bytes)/1000000000); | |
} | |
static bool wasLastDayOfMonth(int year, int month, int day) | |
{ | |
static int lastDays[] = { 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; | |
if (month < 0 || month > 11) | |
return false; | |
if (month == 2) { | |
if (year % 4 == 0 && (year % 100 || year % 400 == 0)) { | |
if (day == 29) | |
return true; | |
return false; | |
} | |
if (day == 28) | |
return true; | |
return false; | |
} | |
return lastDays[month] == day; | |
} | |
static String processFileDateString(const FTPTime& fileTime) | |
{ | |
// FIXME: Need to localize this string? | |
String timeOfDay; | |
if (!(fileTime.tm_hour == 0 && fileTime.tm_min == 0 && fileTime.tm_sec == 0)) { | |
int hour = fileTime.tm_hour; | |
ASSERT(hour >= 0 && hour < 24); | |
if (hour < 12) { | |
if (hour == 0) | |
hour = 12; | |
timeOfDay = String::format(", %i:%02i AM", hour, fileTime.tm_min); | |
} else { | |
hour = hour - 12; | |
if (hour == 0) | |
hour = 12; | |
timeOfDay = String::format(", %i:%02i PM", hour, fileTime.tm_min); | |
} | |
} | |
// If it was today or yesterday, lets just do that - but we have to compare to the current time | |
struct tm now; | |
time_t now_t = time(NULL); | |
getLocalTime(&now_t, &now); | |
// localtime does "year = current year - 1900", compensate for that for readability and comparison purposes | |
now.tm_year += 1900; | |
if (fileTime.tm_year == now.tm_year) { | |
if (fileTime.tm_mon == now.tm_mon) { | |
if (fileTime.tm_mday == now.tm_mday) | |
return "Today" + timeOfDay; | |
if (fileTime.tm_mday == now.tm_mday - 1) | |
return "Yesterday" + timeOfDay; | |
} | |
if (now.tm_mday == 1 && (now.tm_mon == fileTime.tm_mon + 1 || (now.tm_mon == 0 && fileTime.tm_mon == 11)) && | |
wasLastDayOfMonth(fileTime.tm_year, fileTime.tm_mon, fileTime.tm_mday)) | |
return "Yesterday" + timeOfDay; | |
} | |
if (fileTime.tm_year == now.tm_year - 1 && fileTime.tm_mon == 12 && fileTime.tm_mday == 31 && now.tm_mon == 1 && now.tm_mday == 1) | |
return "Yesterday" + timeOfDay; | |
static const char* months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "???" }; | |
int month = fileTime.tm_mon; | |
if (month < 0 || month > 11) | |
month = 12; | |
String dateString; | |
if (fileTime.tm_year > -1) | |
dateString = String::format("%s %i, %i", months[month], fileTime.tm_mday, fileTime.tm_year); | |
else | |
dateString = String::format("%s %i, %i", months[month], fileTime.tm_mday, now.tm_year); | |
return dateString + timeOfDay; | |
} | |
void FTPDirectoryTokenizer::parseAndAppendOneLine(const String& inputLine) | |
{ | |
ListResult result; | |
CString latin1Input = inputLine.latin1(); | |
FTPEntryType typeResult = parseOneFTPLine(latin1Input.data(), m_listState, result); | |
// FTPMiscEntry is a comment or usage statistic which we don't care about, and junk is invalid data - bail in these 2 cases | |
if (typeResult == FTPMiscEntry || typeResult == FTPJunkEntry) | |
return; | |
String filename(result.filename, result.filenameLength); | |
if (result.type == FTPDirectoryEntry) { | |
filename.append("/"); | |
// We have no interest in linking to "current directory" | |
if (filename == "./") | |
return; | |
} | |
LOG(FTP, "Appending entry - %s, %s", filename.ascii().data(), result.fileSize.ascii().data()); | |
appendEntry(filename, processFilesizeString(result.fileSize, result.type == FTPDirectoryEntry), processFileDateString(result.modifiedTime), result.type == FTPDirectoryEntry); | |
} | |
static inline PassRefPtr<SharedBuffer> createTemplateDocumentData(Settings* settings) | |
{ | |
RefPtr<SharedBuffer> buffer = 0; | |
if (settings) | |
buffer = SharedBuffer::createWithContentsOfFile(settings->ftpDirectoryTemplatePath()); | |
if (buffer) | |
LOG(FTP, "Loaded FTPDirectoryTemplate of length %i\n", buffer->size()); | |
return buffer.release(); | |
} | |
bool FTPDirectoryTokenizer::loadDocumentTemplate() | |
{ | |
DEFINE_STATIC_LOCAL(RefPtr<SharedBuffer>, templateDocumentData, (createTemplateDocumentData(m_doc->settings()))); | |
// FIXME: Instead of storing the data, we'd rather actually parse the template data into the template Document once, | |
// store that document, then "copy" it whenever we get an FTP directory listing. There are complexities with this | |
// approach that make it worth putting this off. | |
if (!templateDocumentData) { | |
LOG_ERROR("Could not load templateData"); | |
return false; | |
} | |
// Tokenize the template as an HTML document synchronously | |
setForceSynchronous(true); | |
HTMLTokenizer::write(String(templateDocumentData->data(), templateDocumentData->size()), true); | |
setForceSynchronous(false); | |
RefPtr<Element> tableElement = m_doc->getElementById("ftpDirectoryTable"); | |
if (!tableElement) | |
LOG_ERROR("Unable to find element by id \"ftpDirectoryTable\" in the template document."); | |
else if (!tableElement->hasTagName(tableTag)) | |
LOG_ERROR("Element of id \"ftpDirectoryTable\" is not a table element"); | |
else | |
m_tableElement = static_cast<HTMLTableElement*>(tableElement.get()); | |
// Bail if we found the table element | |
if (m_tableElement) | |
return true; | |
// Otherwise create one manually | |
tableElement = m_doc->createElement(tableTag, false); | |
m_tableElement = static_cast<HTMLTableElement*>(tableElement.get()); | |
ExceptionCode ec; | |
m_tableElement->setAttribute("id", "ftpDirectoryTable", ec); | |
// If we didn't find the table element, lets try to append our own to the body | |
// If that fails for some reason, cram it on the end of the document as a last | |
// ditch effort | |
if (Element* body = m_doc->body()) | |
body->appendChild(m_tableElement, ec); | |
else | |
m_doc->appendChild(m_tableElement, ec); | |
return true; | |
} | |
void FTPDirectoryTokenizer::createBasicDocument() | |
{ | |
LOG(FTP, "Creating a basic FTP document structure as no template was loaded"); | |
// FIXME: Make this "basic document" more acceptable | |
RefPtr<Element> bodyElement = m_doc->createElement(bodyTag, false); | |
ExceptionCode ec; | |
m_doc->appendChild(bodyElement, ec); | |
RefPtr<Element> tableElement = m_doc->createElement(tableTag, false); | |
m_tableElement = static_cast<HTMLTableElement*>(tableElement.get()); | |
m_tableElement->setAttribute("id", "ftpDirectoryTable", ec); | |
bodyElement->appendChild(m_tableElement, ec); | |
} | |
void FTPDirectoryTokenizer::write(const SegmentedString& s, bool /*appendData*/) | |
{ | |
// Make sure we have the table element to append to by loading the template set in the pref, or | |
// creating a very basic document with the appropriate table | |
if (!m_tableElement) { | |
if (!loadDocumentTemplate()) | |
createBasicDocument(); | |
ASSERT(m_tableElement); | |
} | |
bool foundNewLine = false; | |
m_dest = m_buffer; | |
SegmentedString str = s; | |
while (!str.isEmpty()) { | |
UChar c = *str; | |
if (c == '\r') { | |
*m_dest++ = '\n'; | |
foundNewLine = true; | |
// possibly skip an LF in the case of an CRLF sequence | |
m_skipLF = true; | |
} else if (c == '\n') { | |
if (!m_skipLF) | |
*m_dest++ = c; | |
else | |
m_skipLF = false; | |
} else { | |
*m_dest++ = c; | |
m_skipLF = false; | |
} | |
str.advance(); | |
// Maybe enlarge the buffer | |
checkBuffer(); | |
} | |
if (!foundNewLine) { | |
m_dest = m_buffer; | |
return; | |
} | |
UChar* start = m_buffer; | |
UChar* cursor = start; | |
while (cursor < m_dest) { | |
if (*cursor == '\n') { | |
m_carryOver.append(String(start, cursor - start)); | |
LOG(FTP, "%s", m_carryOver.ascii().data()); | |
parseAndAppendOneLine(m_carryOver); | |
m_carryOver = String(); | |
start = ++cursor; | |
} else | |
cursor++; | |
} | |
// Copy the partial line we have left to the carryover buffer | |
if (cursor - start > 1) | |
m_carryOver.append(String(start, cursor - start - 1)); | |
} | |
void FTPDirectoryTokenizer::finish() | |
{ | |
// Possible the last line in the listing had no newline, so try to parse it now | |
if (!m_carryOver.isEmpty()) { | |
parseAndAppendOneLine(m_carryOver); | |
m_carryOver = String(); | |
} | |
m_tableElement = 0; | |
fastFree(m_buffer); | |
HTMLTokenizer::finish(); | |
} | |
FTPDirectoryDocument::FTPDirectoryDocument(Frame* frame) | |
: HTMLDocument(frame) | |
{ | |
#ifndef NDEBUG | |
LogFTP.state = WTFLogChannelOn; | |
#endif | |
} | |
Tokenizer* FTPDirectoryDocument::createTokenizer() | |
{ | |
return new FTPDirectoryTokenizer(this); | |
} | |
} | |
#endif // ENABLE(FTPDIR) |