blob: 22818da6251ac6b80014e28ade1a6c028c8d22db [file] [log] [blame]
/*
* Copyright (C) 2007-2008, 2014-2015 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 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 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"
#include "FTPDirectoryDocument.h"
#if ENABLE(FTPDIR)
#include "HTMLAnchorElement.h"
#include "HTMLBodyElement.h"
#include "HTMLDocumentParser.h"
#include "HTMLTableCellElement.h"
#include "HTMLTableElement.h"
#include "LocalizedStrings.h"
#include "Logging.h"
#include "FTPDirectoryParser.h"
#include "Settings.h"
#include "SharedBuffer.h"
#include "Text.h"
#include <wtf/GregorianDateTime.h>
#include <wtf/IsoMallocInlines.h>
#include <wtf/StdLibExtras.h>
#include <wtf/text/StringConcatenateNumbers.h>
#include <wtf/text/StringToIntegerConversion.h>
#include <wtf/unicode/CharacterNames.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(FTPDirectoryDocument);
using namespace HTMLNames;
class FTPDirectoryDocumentParser final : public HTMLDocumentParser {
public:
static Ref<FTPDirectoryDocumentParser> create(HTMLDocument& document)
{
return adoptRef(*new FTPDirectoryDocumentParser(document));
}
private:
void append(RefPtr<StringImpl>&&) override;
void finish() override;
void checkBuffer(int len = 10)
{
if ((m_dest - m_buffer) > m_size - len) {
// Enlarge buffer
int newSize = std::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;
}
}
FTPDirectoryDocumentParser(HTMLDocument&);
// The parser 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(String&& name, String&& size, String&& date, bool isDirectory);
Ref<Element> createTDForFilename(String&&);
RefPtr<HTMLTableElement> m_tableElement;
bool m_skipLF { false };
int m_size { 254 };
UChar* m_buffer;
UChar* m_dest;
StringBuilder m_carryOver;
ListState m_listState;
};
FTPDirectoryDocumentParser::FTPDirectoryDocumentParser(HTMLDocument& document)
: HTMLDocumentParser(document)
, m_buffer(static_cast<UChar*>(fastMalloc(sizeof(UChar) * m_size)))
, m_dest(m_buffer)
{
}
void FTPDirectoryDocumentParser::appendEntry(String&& filename, String&& size, String&& date, bool isDirectory)
{
auto& document = *this->document();
auto rowElement = m_tableElement->insertRow(-1).releaseReturnValue();
rowElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, "ftpDirectoryEntryRow"_s);
auto typeElement = HTMLTableCellElement::create(tdTag, document);
typeElement->appendChild(Text::create(document, String(&noBreakSpace, 1)));
if (isDirectory)
typeElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, "ftpDirectoryIcon ftpDirectoryTypeDirectory"_s);
else
typeElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, "ftpDirectoryIcon ftpDirectoryTypeFile"_s);
rowElement->appendChild(typeElement);
auto nameElement = createTDForFilename(WTFMove(filename));
nameElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, "ftpDirectoryFileName"_s);
rowElement->appendChild(nameElement);
auto dateElement = HTMLTableCellElement::create(tdTag, document);
dateElement->appendChild(Text::create(document, WTFMove(date)));
dateElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, "ftpDirectoryFileDate"_s);
rowElement->appendChild(dateElement);
auto sizeElement = HTMLTableCellElement::create(tdTag, document);
sizeElement->appendChild(Text::create(document, WTFMove(size)));
sizeElement->setAttributeWithoutSynchronization(HTMLNames::classAttr, "ftpDirectoryFileSize"_s);
rowElement->appendChild(sizeElement);
document.setHasVisuallyNonEmptyCustomContent();
}
Ref<Element> FTPDirectoryDocumentParser::createTDForFilename(String&& filename)
{
auto& document = *this->document();
auto baseURL = document.baseURL().string();
AtomString fullURL;
if (baseURL.endsWith('/'))
fullURL = makeAtomString(baseURL, filename);
else
fullURL = makeAtomString(baseURL, '/', filename);
auto anchorElement = HTMLAnchorElement::create(document);
anchorElement->setAttributeWithoutSynchronization(HTMLNames::hrefAttr, WTFMove(fullURL));
anchorElement->appendChild(Text::create(document, WTFMove(filename)));
auto tdElement = HTMLTableCellElement::create(tdTag, document);
tdElement->appendChild(anchorElement);
return tdElement;
}
static String processFilesizeString(const String& size, bool isDirectory)
{
if (isDirectory)
return "--"_s;
auto bytes = parseIntegerAllowingTrailingJunk<uint64_t>(size);
if (!bytes)
return unknownFileSizeText();
if (*bytes < 1000000)
return makeString(FormattedNumber::fixedWidth(*bytes / 1000.0, 2), " KB");
if (*bytes < 1000000000)
return makeString(FormattedNumber::fixedWidth(*bytes / 1000000.0, 2), " MB");
return makeString(FormattedNumber::fixedWidth(*bytes / 1000000000.0, 2), " GB");
}
static bool wasLastDayOfMonth(int year, int month, int day)
{
static const 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 = makeString(", ", hour, ':', pad('0', 2, fileTime.tm_min), " AM");
} else {
hour = hour - 12;
if (hour == 0)
hour = 12;
timeOfDay = makeString(", ", hour, ':', pad('0', 2, fileTime.tm_min), " PM");
}
}
// If it was today or yesterday, lets just do that - but we have to compare to the current time
GregorianDateTime now;
now.setToCurrentLocalTime();
if (fileTime.tm_year == now.year()) {
if (fileTime.tm_mon == now.month()) {
if (fileTime.tm_mday == now.monthDay())
return "Today" + timeOfDay;
if (fileTime.tm_mday == now.monthDay() - 1)
return "Yesterday" + timeOfDay;
}
if (now.monthDay() == 1 && (now.month() == fileTime.tm_mon + 1 || (now.month() == 0 && fileTime.tm_mon == 11)) &&
wasLastDayOfMonth(fileTime.tm_year, fileTime.tm_mon, fileTime.tm_mday))
return "Yesterday" + timeOfDay;
}
if (fileTime.tm_year == now.year() - 1 && fileTime.tm_mon == 12 && fileTime.tm_mday == 31 && now.month() == 1 && now.monthDay() == 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 = makeString(months[month], ' ', fileTime.tm_mday, ", ", fileTime.tm_year);
else
dateString = makeString(months[month], ' ', fileTime.tm_mday, ", ", now.year());
return dateString + timeOfDay;
}
void FTPDirectoryDocumentParser::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;
if (result.type == FTPDirectoryEntry) {
filename = makeString(StringView { result.filename, result.filenameLength }, '/');
// We have no interest in linking to "current directory"
if (filename == "./"_s)
return;
} else
filename = String(result.filename, result.filenameLength);
LOG(FTP, "Appending entry - %s, %s", filename.ascii().data(), result.fileSize.ascii().data());
appendEntry(WTFMove(filename), processFilesizeString(result.fileSize, result.type == FTPDirectoryEntry), processFileDateString(result.modifiedTime), result.type == FTPDirectoryEntry);
}
static inline RefPtr<SharedBuffer> createTemplateDocumentData(const Settings& settings)
{
auto buffer = SharedBuffer::createWithContentsOfFile(settings.ftpDirectoryTemplatePath());
if (buffer)
LOG(FTP, "Loaded FTPDirectoryTemplate of length %zu\n", buffer->size());
return buffer;
}
bool FTPDirectoryDocumentParser::loadDocumentTemplate()
{
static SharedBuffer* templateDocumentData = createTemplateDocumentData(document()->settings()).leakRef();
// FIXME: Instead of storing the data, it would be more efficient if we could parse the template data into the
// template Document once, store that document, then "copy" it whenever we get an FTP directory listing.
if (!templateDocumentData) {
LOG_ERROR("Could not load templateData");
return false;
}
HTMLDocumentParser::insert(String(templateDocumentData->data(), templateDocumentData->size()));
auto& document = *this->document();
RefPtr foundElement = document.getElementById(StringView { "ftpDirectoryTable"_s });
if (!foundElement)
LOG_ERROR("Unable to find element by id \"ftpDirectoryTable\" in the template document.");
else if (!is<HTMLTableElement>(foundElement))
LOG_ERROR("Element of id \"ftpDirectoryTable\" is not a table element");
else {
m_tableElement = downcast<HTMLTableElement>(foundElement.get());
return true;
}
m_tableElement = HTMLTableElement::create(document);
m_tableElement->setAttributeWithoutSynchronization(HTMLNames::idAttr, "ftpDirectoryTable"_s);
// 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 (RefPtr body = document.bodyOrFrameset())
body->appendChild(*m_tableElement);
else
document.appendChild(*m_tableElement);
return true;
}
void FTPDirectoryDocumentParser::createBasicDocument()
{
LOG(FTP, "Creating a basic FTP document structure as no template was loaded");
auto& document = *this->document();
auto bodyElement = HTMLBodyElement::create(document);
document.appendChild(bodyElement);
m_tableElement = HTMLTableElement::create(document);
m_tableElement->setAttributeWithoutSynchronization(HTMLNames::idAttr, "ftpDirectoryTable"_s);
m_tableElement->setAttribute(HTMLNames::styleAttr, "width:100%"_s);
bodyElement->appendChild(*m_tableElement);
document.processViewport("width=device-width"_s, ViewportArguments::ViewportMeta);
}
void FTPDirectoryDocumentParser::append(RefPtr<StringImpl>&& inputSource)
{
// 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 string { String { WTFMove(inputSource) } };
while (!string.isEmpty()) {
UChar c = string.currentCharacter();
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;
}
string.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(StringView(start, cursor - start));
LOG(FTP, "%s", m_carryOver.toString().ascii().data());
parseAndAppendOneLine(m_carryOver.toString());
m_carryOver.clear();
start = ++cursor;
} else
cursor++;
}
// Copy the partial line we have left to the carryover buffer
if (cursor - start > 1)
m_carryOver.append(StringView(start, cursor - start - 1));
}
void FTPDirectoryDocumentParser::finish()
{
// Possible the last line in the listing had no newline, so try to parse it now
if (!m_carryOver.isEmpty()) {
parseAndAppendOneLine(m_carryOver.toString());
m_carryOver.clear();
}
m_tableElement = nullptr;
fastFree(m_buffer);
HTMLDocumentParser::finish();
}
FTPDirectoryDocument::FTPDirectoryDocument(Frame* frame, const Settings& settings, const URL& url)
: HTMLDocument(frame, settings, url, { })
{
#if !LOG_DISABLED
LogFTP.state = WTFLogChannelState::On;
#endif
}
Ref<DocumentParser> FTPDirectoryDocument::createParser()
{
return FTPDirectoryDocumentParser::create(*this);
}
}
#endif // ENABLE(FTPDIR)