Files
CoreUtils/CGIFormData.cpp
Brad Arant 29db853706 Fix CGIFormData to actually store file-upload part content
File-upload parts (Content-Disposition with filename=) were
unconditionally flagged currentSkip and discarded at PART_BOUNDARY,
so every <field type="file"> upload site (track audio, presets,
project icons) only ever received an empty value via $[fieldname],
never the real uploaded bytes.
2026-09-06 04:04:48 +00:00

174 lines
5.9 KiB
C++

#include "CGIFormData.h"
#include "CGIFormattedData.h"
#include "Exception.h"
namespace coreutils {
CGIFormData::CGIFormData(StreamReader &reader, ZString contentType, int contentLength, ZString boundary)
: Parser(reader), contentType(contentType), contentLength(contentLength), boundary(boundary) {
if (contentType.startsWith("application/x-www-form-urlencoded"))
mode = URLENCODED;
else if (contentType.startsWith("multipart/form-data"))
mode = MULTIPART;
else
mode = UNKNOWN;
}
CGIFormData::~CGIFormData() {
delete headerParser;
delete boundaryParser;
}
void CGIFormData::extractPartName(IMFHeader &header) {
auto &fields = header.getHeaders();
MString disposition = fields[MString("Content-Disposition")];
ZString dz(disposition.getData(), disposition.getLength());
int namePos = dz.find(ZString("name=\""));
if (namePos != -1) {
ZString afterName = dz.substring(namePos + 6);
int nameEnd = afterName.find(ZString("\""));
if (nameEnd != -1)
currentName = MString(afterName.substring(0, nameEnd));
}
}
Parser::Status CGIFormData::parse() {
MString &buffer = reader.getBuffer();
if (mode == UNKNOWN) {
state = DONE;
return Status::COMPLETE;
}
switch (state) {
case INIT:
if (mode == URLENCODED) {
state = URLENCODED_BODY;
return Status::DATA_AVAIL;
}
// MULTIPART: the very first boundary line has no leading CRLF
// (RFC 2046) - handled here directly rather than via IMFBoundary,
// which always expects the CRLF that terminates a PRECEDING part.
// Split into two states (find+consume the marker, then separately
// disambiguate -- vs CRLF) so that if the marker is consumed but
// there isn't yet enough data to tell -- from CRLF, retrying does
// not re-search for (and skip past) a marker that is already
// behind the cursor - the exact bug class already fixed once in
// IMFBoundary's PREFIX/TAIL split.
{
MString marker;
marker << "--";
marker << boundary;
int pos = buffer.find(marker);
if (pos < 0)
return Status::INCOMPLETE;
if (!buffer.ifNext(marker))
throw coreutils::Exception("CGIFormData: opening boundary vanished between find and ifNext.");
state = OPENING_TAIL;
return Status::DATA_AVAIL;
}
case OPENING_TAIL:
if (buffer.getLength() < 2)
return Status::INCOMPLETE;
if (buffer.ifNext(ZString("--"))) {
state = DONE;
return Status::COMPLETE;
}
if (!buffer.ifNext(ZString("\r\n")))
throw coreutils::Exception("CGIFormData: malformed opening boundary line.");
state = PART_HEADERS;
return Status::DATA_AVAIL;
case URLENCODED_BODY:
if ((int)buffer.getLength() < contentLength)
return Status::INCOMPLETE;
{
ZString body(buffer.getCursor(), contentLength);
buffer.setCursor(buffer.getCursor() + contentLength);
CGIFormattedData parsed(body);
for (auto &pair : parsed.data)
storeValue(pair.first, pair.second);
}
state = DONE;
return Status::COMPLETE;
case PART_HEADERS:
if (!headerParser)
headerParser = new IMFHeader(reader);
if (!checkParse(*headerParser))
return Status::INCOMPLETE;
extractPartName(*headerParser);
delete headerParser;
headerParser = NULL;
state = PART_BODY;
return Status::DATA_AVAIL;
case PART_BODY: {
MString marker;
marker << "\r\n--";
marker << boundary;
char *bodyStart = buffer.getCursor();
int pos = buffer.find(marker);
if (pos < 0)
return Status::INCOMPLETE;
char *bodyEnd = buffer.getCursor();
// A part whose Content-Disposition includes filename="..."
// (a real <input type="file">) used to be silently discarded
// here (currentSkip) rather than stored like any other part -
// every existing <field type="file"> upload (track audio,
// presets, project icons) has therefore never actually
// received real file content via $[fieldname], only ever an
// empty value. Storing it the same way as any other part is
// exactly what those call sites already assume.
if (currentName.getLength() > 0)
storeValue(currentName, MString(ZString(bodyStart, bodyEnd - bodyStart)));
currentName = MString();
state = PART_BOUNDARY;
return Status::DATA_AVAIL;
}
case PART_BOUNDARY:
if (!boundaryParser)
boundaryParser = new IMFBoundary(reader, boundary);
if (!checkParse(*boundaryParser))
return Status::INCOMPLETE;
{
bool last = boundaryParser->isLast();
delete boundaryParser;
boundaryParser = NULL;
if (last) {
state = DONE;
return Status::COMPLETE;
}
state = PART_HEADERS;
return Status::DATA_AVAIL;
}
case DONE:
return Status::COMPLETE;
}
throw coreutils::Exception("CGIFormData: unreachable state.");
}
void CGIFormData::storeValue(MString name, MString value) {
int index = nameCounts[name]++;
MString key = name;
key << ":";
key << MString(std::to_string(index));
data[key] = value;
}
MString CGIFormData::operator[](ZString name) {
MString key(name);
if (key.find(ZString(":")) == -1) {
key << ":0";
}
return data[key];
}
}