collabora-online/wsd/ClientRequestDispatcher.cpp

2029 lines
79 KiB
C++
Raw Normal View History

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* Copyright the Collabora Online contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <config.h>
#include <config_version.h>
#include <ClientRequestDispatcher.hpp>
#if ENABLE_FEATURE_LOCK
#include "CommandControl.hpp"
#endif
#include <Admin.hpp>
#include <COOLWSD.hpp>
#include <ClientSession.hpp>
#include <ConfigUtil.hpp>
#include <DocumentBroker.hpp>
#include <Exceptions.hpp>
#include <FileServer.hpp>
#include <HttpRequest.hpp>
#include <JailUtil.hpp>
#include <ProofKey.hpp>
#include <ProxyRequestHandler.hpp>
#include <RequestDetails.hpp>
#include <Socket.hpp>
#include <UserMessages.hpp>
#include <Util.hpp>
#include <net/AsyncDNS.hpp>
#include <net/HttpHelper.hpp>
#if !MOBILEAPP
#include <HostUtil.hpp>
#endif // !MOBILEAPP
#include <Poco/DOM/AutoPtr.h>
#include <Poco/DOM/DOMParser.h>
#include <Poco/DOM/DOMWriter.h>
#include <Poco/DOM/Document.h>
#include <Poco/DOM/Element.h>
#include <Poco/DOM/NodeList.h>
#include <Poco/File.h>
#include <Poco/MemoryStream.h>
#include <Poco/Net/HTMLForm.h>
#include <Poco/Net/NetException.h>
#include <Poco/Net/PartHandler.h>
#include <Poco/SAX/InputSource.h>
#include <Poco/StreamCopier.h>
#include <map>
#include <memory>
#include <string>
#include <vector>
std::map<std::string, std::string> ClientRequestDispatcher::StaticFileContentCache;
std::unordered_map<std::string, std::shared_ptr<RequestVettingStation>>
ClientRequestDispatcher::RequestVettingStations;
extern std::map<std::string, std::shared_ptr<DocumentBroker>> DocBrokers;
extern std::mutex DocBrokersMutex;
extern void cleanupDocBrokers();
namespace
{
/// Used in support key enabled builds
inline void shutdownLimitReached(const std::shared_ptr<ProtocolHandlerInterface>& proto)
{
if (!proto)
return;
const std::string error = Poco::format(PAYLOAD_UNAVAILABLE_LIMIT_REACHED, COOLWSD::MaxDocuments,
COOLWSD::MaxConnections);
LOG_INF("Sending client 'hardlimitreached' message: " << error);
try
{
// Let the client know we are shutting down.
proto->sendTextMessage(error);
// Shutdown.
proto->shutdown(true, error);
}
catch (const std::exception& ex)
{
LOG_ERR("Error while shutting down socket on reaching limit: " << ex.what());
}
}
} // end anonymous namespace
/// Find the DocumentBroker for the given docKey, if one exists.
/// Otherwise, creates and adds a new one to DocBrokers.
/// May return null if terminating or MaxDocuments limit is reached.
/// Returns the error message, if any, when no DocBroker is created/found.
std::pair<std::shared_ptr<DocumentBroker>, std::string>
findOrCreateDocBroker(DocumentBroker::ChildType type, const std::string& uri,
const std::string& docKey, const std::string& id, const Poco::URI& uriPublic,
unsigned mobileAppDocId,
std::unique_ptr<WopiStorage::WOPIFileInfo> wopiFileInfo)
{
LOG_INF("Find or create DocBroker for docKey ["
<< docKey << "] for session [" << id << "] on url ["
<< COOLWSD::anonymizeUrl(uriPublic.toString()) << ']');
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
cleanupDocBrokers();
if (SigUtil::getShutdownRequestFlag())
{
// TerminationFlag implies ShutdownRequested.
LOG_WRN((SigUtil::getTerminationFlag() ? "TerminationFlag" : "ShudownRequestedFlag")
<< " set. Not loading new session [" << id << "] for docKey [" << docKey << ']');
return std::make_pair(nullptr, "error: cmd=load kind=recycling");
}
std::shared_ptr<DocumentBroker> docBroker;
// Lookup this document.
const auto it = DocBrokers.find(docKey);
if (it != DocBrokers.end() && it->second)
{
// Get the DocumentBroker from the Cache.
LOG_DBG("Found DocumentBroker with docKey [" << docKey << ']');
docBroker = it->second;
// Destroying the document? Let the client reconnect.
if (docBroker->isUnloadingUnrecoverably())
{
LOG_WRN("DocBroker [" << docKey
<< "] is unloading. Rejecting client request to load session ["
<< id << ']');
return std::make_pair(nullptr, "error: cmd=load kind=docunloading");
}
}
else
{
LOG_DBG("No DocumentBroker with docKey [" << docKey
<< "] found. Creating new Child and Document");
}
if (SigUtil::getShutdownRequestFlag())
{
// TerminationFlag implies ShutdownRequested.
LOG_ERR((SigUtil::getTerminationFlag() ? "TerminationFlag" : "ShudownRequestedFlag")
<< " set. Not loading new session [" << id << "] for docKey [" << docKey << ']');
return std::make_pair(nullptr, "error: cmd=load kind=recycling");
}
if (!docBroker)
{
Util::assertIsLocked(DocBrokersMutex);
if (DocBrokers.size() + 1 > COOLWSD::MaxDocuments)
{
LOG_WRN("Maximum number of open documents of "
<< COOLWSD::MaxDocuments << " reached while loading new session [" << id
<< "] for docKey [" << docKey << ']');
if (config::isSupportKeyEnabled())
{
const std::string error = Poco::format(PAYLOAD_UNAVAILABLE_LIMIT_REACHED,
COOLWSD::MaxDocuments, COOLWSD::MaxConnections);
return std::make_pair(nullptr, error);
}
}
// Set the one we just created.
LOG_DBG("New DocumentBroker for docKey [" << docKey << ']');
docBroker = std::make_shared<DocumentBroker>(type, uri, uriPublic, docKey, mobileAppDocId,
std::move(wopiFileInfo));
DocBrokers.emplace(docKey, docBroker);
LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after inserting [" << docKey << ']');
}
return std::make_pair(docBroker, std::string());
}
#if !MOBILEAPP
/// For clipboard setting
class ClipboardPartHandler : public Poco::Net::PartHandler
{
std::shared_ptr<std::string> _data; // large.
public:
std::shared_ptr<std::string> getData() const { return _data; }
ClipboardPartHandler() {}
virtual void handlePart(const Poco::Net::MessageHeader& /* header */,
std::istream& stream) override
{
std::istreambuf_iterator<char> eos;
_data = std::make_shared<std::string>(std::istreambuf_iterator<char>(stream), eos);
LOG_TRC("Clipboard stream from part header stored of size " << _data->length());
}
};
/// Handles the filename part of the convert-to POST request payload,
/// Also owns the file - cleaning it up when destroyed.
class ConvertToPartHandler : public Poco::Net::PartHandler
{
std::string _filename;
public:
std::string getFilename() const { return _filename; }
/// Afterwards someone else is responsible for cleaning that up.
void takeFile() { _filename.clear(); }
ConvertToPartHandler() {}
virtual ~ConvertToPartHandler()
{
if (!_filename.empty())
{
LOG_TRC("Remove un-handled temporary file '" << _filename << '\'');
StatelessBatchBroker::removeFile(_filename);
}
}
virtual void handlePart(const Poco::Net::MessageHeader& header, std::istream& stream) override
{
// Extract filename and put it to a temporary directory.
std::string disp;
Poco::Net::NameValueCollection params;
if (header.has("Content-Disposition"))
{
std::string cd = header.get("Content-Disposition");
Poco::Net::MessageHeader::splitParameters(cd, disp, params);
}
if (!params.has("filename"))
return;
// The temporary directory is child-root/<CHILDROOT_TMP_INCOMING_PATH>.
// Always create a random sub-directory to avoid file-name collision.
Poco::Path tempPath = Poco::Path::forDirectory(
FileUtil::createRandomTmpDir(COOLWSD::ChildRoot +
JailUtil::CHILDROOT_TMP_INCOMING_PATH) +
'/');
LOG_TRC("Created temporary convert-to/insert path: " << tempPath.toString());
// Prevent user inputing anything funny here.
std::string fileParam = params.get("filename");
std::string cleanFilename = Util::cleanupFilename(fileParam);
if (fileParam != cleanFilename)
LOG_DBG("Unexpected characters in conversion filename '"
<< fileParam << "' cleaned to '" << cleanFilename << "'");
// A "filename" should always be a filename, not a path
const Poco::Path filenameParam(cleanFilename);
if (filenameParam.getFileName() == "callback:")
tempPath.setFileName("incoming_file"); // A sensible name.
else
tempPath.setFileName(filenameParam.getFileName()); //TODO: Sanitize.
_filename = tempPath.toString();
LOG_DBG("Storing incoming file to: " << _filename);
// Copy the stream to _filename.
std::ofstream fileStream;
fileStream.open(_filename);
Poco::StreamCopier::copyStream(stream, fileStream);
fileStream.close();
}
};
class RenderSearchResultPartHandler : public Poco::Net::PartHandler
{
private:
std::string _filename;
std::shared_ptr<std::vector<char>> _pSearchResultContent;
public:
std::string getFilename() const { return _filename; }
/// Afterwards someone else is responsible for cleaning that up.
void takeFile() { _filename.clear(); }
const std::shared_ptr<std::vector<char>>& getSearchResultContent() const
{
return _pSearchResultContent;
}
RenderSearchResultPartHandler() = default;
virtual ~RenderSearchResultPartHandler()
{
if (!_filename.empty())
{
LOG_TRC("Remove un-handled temporary file '" << _filename << '\'');
StatelessBatchBroker::removeFile(_filename);
}
}
virtual void handlePart(const Poco::Net::MessageHeader& header, std::istream& stream) override
{
// Extract filename and put it to a temporary directory.
std::string label;
Poco::Net::NameValueCollection content;
if (header.has("Content-Disposition"))
{
Poco::Net::MessageHeader::splitParameters(header.get("Content-Disposition"), label,
content);
}
std::string name = content.get("name", "");
if (name == "document")
{
std::string filename = content.get("filename", "");
const Poco::Path filenameParam(filename);
// The temporary directory is child-root/<JAIL_TMP_INCOMING_PATH>.
// Always create a random sub-directory to avoid file-name collision.
Poco::Path tempPath = Poco::Path::forDirectory(
FileUtil::createRandomTmpDir(COOLWSD::ChildRoot +
JailUtil::CHILDROOT_TMP_INCOMING_PATH) +
'/');
LOG_TRC("Created temporary render-search-result file path: " << tempPath.toString());
// Prevent user inputting anything funny here.
// A "filename" should always be a filename, not a path
if (filenameParam.getFileName() == "callback:")
tempPath.setFileName("incoming_file"); // A sensible name.
else
tempPath.setFileName(filenameParam.getFileName()); //TODO: Sanitize.
_filename = tempPath.toString();
// Copy the stream to _filename.
std::ofstream fileStream;
fileStream.open(_filename);
Poco::StreamCopier::copyStream(stream, fileStream);
fileStream.close();
}
else if (name == "result")
{
// Copy content from the stream into a std::vector<char>
_pSearchResultContent = std::make_shared<std::vector<char>>(
std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
}
}
};
/// Constructs ConvertToBroker implamentation based on request type
std::shared_ptr<ConvertToBroker>
getConvertToBrokerImplementation(const std::string& requestType, const std::string& fromPath,
const Poco::URI& uriPublic, const std::string& docKey,
const std::string& format, const std::string& options,
const std::string& lang, const std::string& target)
{
if (requestType == "convert-to")
return std::make_shared<ConvertToBroker>(fromPath, uriPublic, docKey, format, options,
lang);
else if (requestType == "extract-link-targets")
return std::make_shared<ExtractLinkTargetsBroker>(fromPath, uriPublic, docKey, lang);
else if (requestType == "get-thumbnail")
return std::make_shared<GetThumbnailBroker>(fromPath, uriPublic, docKey, lang, target);
return nullptr;
}
class ConvertToAddressResolver : public std::enable_shared_from_this<ConvertToAddressResolver>
{
std::shared_ptr<ConvertToAddressResolver> _selfLifecycle;
std::vector<std::string> _addressesToResolve;
ClientRequestDispatcher::AsyncFn _asyncCb;
bool _allow;
public:
ConvertToAddressResolver(std::vector<std::string> addressesToResolve, ClientRequestDispatcher::AsyncFn asyncCb)
: _addressesToResolve(std::move(addressesToResolve))
, _asyncCb(asyncCb)
, _allow(true)
{
}
void testHostName(const std::string& hostToCheck)
{
_allow &= HostUtil::allowedWopiHost(hostToCheck);
}
// synchronous case
bool syncProcess()
{
assert(!_asyncCb);
while (!_addressesToResolve.empty())
{
const std::string& addressToCheck = _addressesToResolve.front();
try
{
std::string resolvedHostName = net::canonicalHostName(addressToCheck);
testHostName(resolvedHostName);
}
catch (const Poco::Exception& exc)
{
LOG_ERR_S("net::canonicalHostName(\"" << addressToCheck
<< "\") failed: " << exc.displayText());
// We can't find out the hostname, and it already failed the IP check
_allow = false;
}
if (_allow)
{
LOG_INF_S("convert-to: Requesting address is allowed: " << addressToCheck);
}
else
{
LOG_WRN_S("convert-to: Requesting address is denied: " << addressToCheck);
break;
}
_addressesToResolve.pop_back();
}
return _allow;
}
// asynchronous case
void startAsyncProcessing()
{
assert(_asyncCb);
_selfLifecycle = shared_from_this();
dispatchNextLookup();
}
std::string toState() const
{
std::string state = "ConvertToAddressResolver: ";
for (const auto& address : _addressesToResolve)
state += address + ", ";
state += "\n";
return state;
}
void dispatchNextLookup()
{
net::AsyncDNS::DNSThreadFn pushHostnameResolvedToPoll = [this](const std::string& hostname,
const std::string& exception) {
COOLWSD::getWebServerPoll()->addCallback([this, hostname, exception]() {
hostnameResolved(hostname, exception);
});
};
net::AsyncDNS::DNSThreadDumpStateFn dumpState = [this]() -> std::string {
return toState();
};
const std::string& addressToCheck = _addressesToResolve.front();
net::AsyncDNS::canonicalHostName(addressToCheck, pushHostnameResolvedToPoll, dumpState);
}
void hostnameResolved(const std::string& hostToCheck, const std::string& exception)
{
if (!exception.empty())
{
LOG_ERR_S(exception);
// We can't find out the hostname, and it already failed the IP check
_allow = false;
}
else
testHostName(hostToCheck);
const std::string& addressToCheck = _addressesToResolve.front();
if (_allow)
LOG_INF_S("convert-to: Requesting address is allowed: " << addressToCheck);
else
LOG_WRN_S("convert-to: Requesting address is denied: " << addressToCheck);
_addressesToResolve.pop_back();
// If hostToCheck is not allowed, or there are no addresses
// left to check, then do callback and end
if (!_allow || _addressesToResolve.empty())
{
_asyncCb(_allow);
_selfLifecycle.reset();
return;
}
dispatchNextLookup();
}
};
bool ClientRequestDispatcher::allowPostFrom(const std::string& address)
{
static bool init = false;
static Util::RegexListMatcher hosts;
if (!init)
{
const auto& app = Poco::Util::Application::instance();
// Parse the host allow settings.
for (size_t i = 0;; ++i)
{
const std::string path = "net.post_allow.host[" + std::to_string(i) + ']';
const auto host = app.config().getString(path, "");
if (!host.empty())
{
LOG_INF_S("Adding trusted POST_ALLOW host: [" << host << ']');
hosts.allow(host);
}
else if (!app.config().has(path))
{
break;
}
}
init = true;
}
return hosts.match(address);
}
bool ClientRequestDispatcher::allowConvertTo(const std::string& address,
const Poco::Net::HTTPRequest& request,
AsyncFn asyncCb)
{
const bool allow = allowPostFrom(address) || HostUtil::allowedWopiHost(request.getHost());
if (!allow)
{
LOG_WRN_S("convert-to: Requesting address is denied: " << address);
if (asyncCb)
asyncCb(false);
return false;
}
LOG_TRC_S("convert-to: Requesting address is allowed: " << address);
std::vector<std::string> addressesToResolve;
// Handle forwarded header and make sure all participating IPs are allowed
if (request.has("X-Forwarded-For"))
{
const std::string fowardedData = request.get("X-Forwarded-For");
StringVector tokens = StringVector::tokenize(fowardedData, ',');
for (const auto& token : tokens)
{
std::string param = tokens.getParam(token);
std::string addressToCheck = Util::trim(param);
if (!allowPostFrom(addressToCheck))
{
// postpone resolving addresses until later
addressesToResolve.push_back(addressToCheck);
continue;
}
LOG_INF_S("convert-to: Requesting address is allowed: " << addressToCheck);
}
}
if (addressesToResolve.empty())
{
if (asyncCb)
asyncCb(true);
return true;
}
auto resolver = std::make_shared<ConvertToAddressResolver>(std::move(addressesToResolve), asyncCb);
if (asyncCb)
{
resolver->startAsyncProcessing();
return false;
}
return resolver->syncProcess();
}
#endif // !MOBILEAPP
void ClientRequestDispatcher::onConnect(const std::shared_ptr<StreamSocket>& socket)
{
_id = COOLWSD::GetConnectionId();
_socket = socket;
setLogContext(socket->getFD());
LOG_TRC("Connected to ClientRequestDispatcher");
}
void ClientRequestDispatcher::handleIncomingMessage(SocketDisposition& disposition)
{
std::shared_ptr<StreamSocket> socket = _socket.lock();
if (!socket)
{
LOG_ERR("Invalid socket while handling incoming client request");
return;
}
#if !MOBILEAPP
if (!COOLWSD::isSSLEnabled() && socket->sniffSSL())
{
LOG_ERR("Looks like SSL/TLS traffic on plain http port");
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
return;
}
Poco::MemoryInputStream startmessage(&socket->getInBuffer()[0], socket->getInBuffer().size());
#if 0 // debug a specific command's payload
if (Util::findInVector(socket->getInBuffer(), "insertfile") != std::string::npos)
{
std::ostringstream oss;
oss << "Debug - specific command:\n";
socket->dumpState(oss);
LOG_INF(oss.str());
}
#endif
Poco::Net::HTTPRequest request;
StreamSocket::MessageMap map;
if (!socket->parseHeader("Client", startmessage, request, map))
return;
LOG_DBG("Handling request: " << request.getURI());
try
{
// We may need to re-write the chunks moving the inBuffer.
socket->compactChunks(map);
Poco::MemoryInputStream message(&socket->getInBuffer()[0], socket->getInBuffer().size());
// update the read cursor - headers are not altered by chunks.
message.seekg(startmessage.tellg(), std::ios::beg);
// re-write ServiceRoot and cache.
RequestDetails requestDetails(request, COOLWSD::ServiceRoot);
// LOG_TRC("Request details " << requestDetails.toString());
// Config & security ...
if (requestDetails.isProxy())
{
if (!COOLWSD::IsProxyPrefixEnabled)
throw BadRequestException(
"ProxyPrefix present but net.proxy_prefix is not enabled");
else if (!socket->isLocal())
throw BadRequestException("ProxyPrefix request from non-local socket");
}
// Routing
if (UnitWSD::isUnitTesting() && UnitWSD::get().handleHttpRequest(request, message, socket))
{
// Unit testing, nothing to do here
}
else if (requestDetails.equals(RequestDetails::Field::Type, "browser") ||
requestDetails.equals(RequestDetails::Field::Type, "wopi"))
{
// File server
assert(socket && "Must have a valid socket");
constexpr auto ProxyRemote = "/remote/";
constexpr auto ProxyRemoteLen = sizeof(ProxyRemote) - 1;
constexpr auto ProxyRemoteStatic = "/remote/static/";
const auto uri = requestDetails.getURI();
const auto pos = uri.find(ProxyRemoteStatic);
if (pos != std::string::npos)
{
if (uri.ends_with("lokit-extra-img.svg"))
{
ProxyRequestHandler::handleRequest(uri.substr(pos + ProxyRemoteLen), socket,
ProxyRequestHandler::getProxyRatingServer());
}
#if ENABLE_FEATURE_LOCK
else
{
const Poco::URI unlockImageUri =
CommandControl::LockManager::getUnlockImageUri();
if (!unlockImageUri.empty())
{
const std::string& serverUri =
unlockImageUri.getScheme() + "://" + unlockImageUri.getAuthority();
ProxyRequestHandler::handleRequest(
uri.substr(pos + sizeof("/remote/static") - 1), socket, serverUri);
}
}
#endif
}
else
{
FileServerRequestHandler::ResourceAccessDetails accessDetails;
COOLWSD::FileRequestHandler->handleRequest(request, requestDetails, message, socket,
accessDetails);
if (accessDetails.isValid())
{
LOG_ASSERT_MSG(Util::decodeURIComponent(
requestDetails.getField(RequestDetails::Field::WOPISrc)) ==
Util::decodeURIComponent(accessDetails.wopiSrc()),
"Expected identical WOPISrc in the request as in cool.html");
const std::string requestKey = RequestDetails::getRequestKey(
accessDetails.wopiSrc(), accessDetails.accessToken());
std::vector<std::string> options = {
"access_token=" + accessDetails.accessToken(), "access_token_ttl=0"
};
if (!accessDetails.permission().empty())
options.push_back("permission=" + accessDetails.permission());
const RequestDetails fullRequestDetails =
RequestDetails(accessDetails.wopiSrc(), options, /*compat=*/std::string());
if (RequestVettingStations.find(requestKey) != RequestVettingStations.end())
{
LOG_TRC("Found RVS under key: " << requestKey << ", nothing to do");
}
else
{
LOG_TRC("Creating RVS with key: " << requestKey << ", for DocumentLoadURI: "
<< fullRequestDetails.getDocumentURI());
auto it = RequestVettingStations.emplace(
requestKey, std::make_shared<RequestVettingStation>(
COOLWSD::getWebServerPoll(), fullRequestDetails));
it.first->second->handleRequest(_id);
}
}
socket->shutdown();
}
}
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(1, "adminws"))
{
// Admin connections
LOG_INF("Admin request: " << request.getURI());
if (AdminSocketHandler::handleInitialRequest(_socket, request))
{
disposition.setMove(
[](const std::shared_ptr<Socket>& moveSocket)
{
// Hand the socket over to the Admin poll.
Admin::instance().insertNewSocket(moveSocket);
});
}
}
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(1, "getMetrics"))
{
if (!COOLWSD::AdminEnabled)
throw Poco::FileAccessDeniedException("Admin console disabled");
// See metrics.txt
std::shared_ptr<http::Response> response =
std::make_shared<http::Response>(http::StatusCode::OK);
try
{
/* WARNING: security point, we may skip authentication */
bool skipAuthentication =
COOLWSD::getConfigValue<bool>("security.enable_metrics_unauthenticated", false);
if (!skipAuthentication)
if (!COOLWSD::FileRequestHandler->isAdminLoggedIn(request, *response))
throw Poco::Net::NotAuthenticatedException("Invalid admin login");
}
catch (const Poco::Net::NotAuthenticatedException& exc)
{
//LOG_ERR("FileServerRequestHandler::NotAuthenticated: " << exc.displayText());
http::Response httpResponse(http::StatusCode::Unauthorized);
httpResponse.set("Content-Type", "text/html charset=UTF-8");
httpResponse.set("WWW-authenticate", "Basic realm=\"online\"");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
FileServerRequestHandler::hstsHeaders(*response);
response->add("Last-Modified", Util::getHttpTimeNow());
// Ask UAs to block if they detect any XSS attempt
response->add("X-XSS-Protection", "1; mode=block");
// No referrer-policy
response->add("Referrer-Policy", "no-referrer");
response->add("X-Content-Type-Options", "nosniff");
disposition.setTransfer(Admin::instance(),
[response](const std::shared_ptr<Socket>& moveSocket)
{
const std::shared_ptr<StreamSocket> streamSocket =
std::static_pointer_cast<StreamSocket>(moveSocket);
Admin::instance().sendMetrics(streamSocket, response);
});
}
else if (requestDetails.isGetOrHead("/"))
handleRootRequest(requestDetails, socket);
else if (requestDetails.isGet("/favicon.ico"))
handleFaviconRequest(requestDetails, socket);
else if (requestDetails.equals(0, "hosting"))
{
if (requestDetails.equals(1, "discovery"))
handleWopiDiscoveryRequest(requestDetails, socket);
else if (requestDetails.equals(1, "capabilities"))
handleCapabilitiesRequest(request, socket);
}
else if (requestDetails.isGet("/robots.txt"))
handleRobotsTxtRequest(request, socket);
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(1, "media"))
{
handleMediaRequest(request, disposition, socket);
}
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(1, "clipboard"))
{
// Util::dumpHex(std::cerr, socket->getInBuffer(), "clipboard:\n"); // lots of data ...
handleClipboardRequest(request, message, disposition, socket);
}
else if (requestDetails.isProxy() && requestDetails.equals(2, "ws"))
handleClientProxyRequest(request, requestDetails, message, disposition);
else if (requestDetails.equals(RequestDetails::Field::Type, "cool") &&
requestDetails.equals(2, "ws") && requestDetails.isWebSocket())
handleClientWsUpgrade(request, requestDetails, disposition, socket);
else if (!requestDetails.isWebSocket() &&
(requestDetails.equals(RequestDetails::Field::Type, "cool") ||
requestDetails.equals(RequestDetails::Field::Type, "lool")))
{
// All post requests have url prefix 'cool', except when the prefix
// is 'lool' e.g. when integrations use the old /lool/convert-to endpoint
handlePostRequest(requestDetails, request, message, disposition, socket);
}
else if (requestDetails.equals(RequestDetails::Field::Type, "wasm"))
{
if (COOLWSD::WASMState == COOLWSD::WASMActivationState::Disabled)
{
LOG_ERR(
"WASM document request while WASM is disabled: " << requestDetails.toString());
// Bad request.
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
return;
}
// Tunnel to WASM.
_wopiProxy = std::make_unique<WopiProxy>(_id, requestDetails, socket);
_wopiProxy->handleRequest(COOLWSD::getWebServerPoll(), disposition);
}
else
{
LOG_ERR("Unknown resource: " << requestDetails.toString());
// Bad request.
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
return;
}
}
catch (const BadRequestException& ex)
{
LOG_ERR('#' << socket->getFD() << " bad request: ["
<< COOLProtocol::getAbbreviatedMessage(socket->getInBuffer())
<< "]: " << ex.what());
// Bad request.
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
return;
}
catch (const std::exception& exc)
{
LOG_ERR('#' << socket->getFD() << " Exception while processing incoming request: ["
<< COOLProtocol::getAbbreviatedMessage(socket->getInBuffer())
<< "]: " << exc.what());
// Bad request.
// NOTE: Check _wsState to choose between HTTP response or WebSocket (app-level) error.
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
// if we succeeded - remove the request from our input buffer
// we expect one request per socket
socket->eraseFirstInputBytes(map);
#else // !MOBILEAPP
Poco::Net::HTTPRequest request;
#ifdef IOS
// The URL of the document is sent over the FakeSocket by the code in
// -[DocumentViewController userContentController:didReceiveScriptMessage:] when it gets the
// HULLO message from the JavaScript in global.js.
// The "app document id", the numeric id of the document, from the appDocIdCounter in CODocument.mm.
char* space = strchr(socket->getInBuffer().data(), ' ');
assert(space != nullptr);
// The socket buffer is not nul-terminated so we can't just call strtoull() on the number at
// its end, it might be followed in memory by more digits. Is there really no better way to
// parse the number at the end of the buffer than to copy the bytes into a nul-terminated
// buffer?
const size_t appDocIdLen =
(socket->getInBuffer().data() + socket->getInBuffer().size()) - (space + 1);
char* appDocIdBuffer = (char*)malloc(appDocIdLen + 1);
memcpy(appDocIdBuffer, space + 1, appDocIdLen);
appDocIdBuffer[appDocIdLen] = '\0';
unsigned appDocId = std::strtoul(appDocIdBuffer, nullptr, 10);
free(appDocIdBuffer);
handleClientWsUpgrade(
request, std::string(socket->getInBuffer().data(), space - socket->getInBuffer().data()),
disposition, socket, appDocId);
#else // IOS
handleClientWsUpgrade(
request,
RequestDetails(std::string(socket->getInBuffer().data(), socket->getInBuffer().size())),
disposition, socket);
#endif // !IOS
socket->getInBuffer().clear();
#endif // MOBILEAPP
}
#if !MOBILEAPP
void ClientRequestDispatcher::handleRootRequest(const RequestDetails& requestDetails,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG("HTTP request: " << requestDetails.getURI());
const std::string mimeType = "text/plain";
const std::string responseString = "OK";
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Content-Length", std::to_string(responseString.size()));
httpResponse.set("Content-Type", mimeType);
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.set("Connection", "close");
httpResponse.writeData(socket->getOutBuffer());
if (requestDetails.isGet())
socket->send(responseString);
socket->flush();
socket->shutdown();
LOG_INF("Sent / response successfully.");
}
void ClientRequestDispatcher::handleFaviconRequest(const RequestDetails& requestDetails,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_TRC_S("Favicon request: " << requestDetails.getURI());
http::Response response(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(response);
response.setContentType("image/vnd.microsoft.icon");
std::string faviconPath =
Poco::Path(Poco::Util::Application::instance().commandPath()).parent().toString() +
"favicon.ico";
if (!Poco::File(faviconPath).exists())
faviconPath = COOLWSD::FileServerRoot + "/favicon.ico";
HttpHelper::sendFileAndShutdown(socket, faviconPath, response);
}
void ClientRequestDispatcher::handleWopiDiscoveryRequest(
const RequestDetails& requestDetails, const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG("Wopi discovery request: " << requestDetails.getURI());
std::string xml = getFileContent("discovery.xml");
std::string srvUrl =
#if ENABLE_SSL
((COOLWSD::isSSLEnabled() || COOLWSD::isSSLTermination()) ? "https://" : "http://")
#else
"http://"
#endif
+ (COOLWSD::ServerName.empty() ? requestDetails.getHostUntrusted() : COOLWSD::ServerName) +
COOLWSD::ServiceRoot;
if (requestDetails.isProxy())
srvUrl = requestDetails.getProxyPrefix();
Poco::replaceInPlace(xml, std::string("%SRV_URI%"), srvUrl);
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.setBody(xml, "text/xml");
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.set("X-Content-Type-Options", "nosniff");
LOG_TRC("Sending back discovery.xml: " << xml);
socket->sendAndShutdown(httpResponse);
LOG_INF("Sent discovery.xml successfully.");
}
void ClientRequestDispatcher::handleClipboardRequest(const Poco::Net::HTTPRequest& request,
Poco::MemoryInputStream& message,
SocketDisposition& disposition,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG_S(
"Clipboard " << ((request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET) ? "GET" : "POST")
<< " request: " << request.getURI());
Poco::URI requestUri(request.getURI());
Poco::URI::QueryParameters params = requestUri.getQueryParameters();
std::string WOPISrc, serverId, viewId, tag, mime;
for (const auto& it : params)
{
if (it.first == "WOPISrc")
WOPISrc = it.second;
else if (it.first == "ServerId")
serverId = it.second;
else if (it.first == "ViewId")
viewId = it.second;
else if (it.first == "Tag")
tag = it.second;
else if (it.first == "MimeType")
mime = it.second;
}
if (serverId != Util::getProcessIdentifier())
{
LOG_ERR_S("Cluster configuration error: mis-matching serverid ["
<< serverId << "] vs. [" << Util::getProcessIdentifier() << "] with tag [" << tag
<< "] on request to URL: " << request.getURI());
// we got the wrong request.
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
// Verify that the WOPISrc is properly encoded.
if (!HttpHelper::verifyWOPISrc(request.getURI(), WOPISrc, socket))
{
return;
}
const auto docKey = RequestDetails::getDocKey(WOPISrc);
LOG_TRC_S("Clipboard request for us: [" << serverId << "] with tag [" << tag << "] on docKey ["
<< docKey << ']');
std::shared_ptr<DocumentBroker> docBroker;
{
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto it = DocBrokers.find(docKey);
if (it != DocBrokers.end())
docBroker = it->second;
}
// If we have a valid docBroker, use it.
// Note: there is a race here as DocBroker may
// have already exited its SocketPoll, but we
// haven't cleaned up the DocBrokers container.
// Since we don't care about creating a new one,
// we simply go to the fallback below.
if (docBroker && docBroker->isAlive())
{
std::shared_ptr<std::string> data;
DocumentBroker::ClipboardRequest type;
if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET)
{
if (mime == "text/html")
type = DocumentBroker::CLIP_REQUEST_GET_RICH_HTML_ONLY;
cool#8465 clipboard: improve handling of plain text copy, complex case In case the selection is complex (not simple), we used to just request HTML, and then the browser converted that to plain text, which has the downsides already mentioned in commit 7f9de46688a64b42ba8f65cceb9fe2c6ddab89ef (cool#8465 clipboard: improve handling of plain text copy, simple case, 2024-03-08). Steps to support this: 1) Clipboard.js now asks for the text/html,text/plain;charset=utf-8 MIME types. 2) wsd: ClientRequestDispatcher::handleClipboardRequest() now maps this to DocumentBroker::CLIP_REQUEST_GET_HTML_PLAIN_ONLY 3) ClientSession::handleClipboardRequest() maps this to the HTML+plain text MIME type list. 4) kit: ChildSession::getClipboard() is now improved to take a list of MIME types, not just 1 or everything. 5) kit: ChildSession::getClipboard() now emits JSON in case not all, but multiple MIME types are requested. 6) wsd: ClientSession::postProcessCopyPayload() now knows how to postprocess clipboardcontent messages, which may or may not be JSON (it's JSON if more formats are requested explicitly, leaving the 1 format or all format cases unchanged) 7) Control.DownloadProgress.js now handles the case when we get JSON and sets the core-provided plain text next to the HTML. Leave the handling of non-JSON case in, because this means we can copy from an old COOL server to a new one. Note that this approach has the benefit that once the clipboard marker is inserted, the length of the text/html format would change, which means we can't parse the clipboard data till the marker is removed. Emitting JSON for html+text means adding the marker keeps the ability to parse the HTML and the plain text part of the clipboard in JS. Signed-off-by: Miklos Vajna <vmiklos@collabora.com> Change-Id: I67a1f669e8a638d34cc25a2f288a7b30884b9892
2024-03-19 12:12:16 +01:00
else if (mime == "text/html,text/plain;charset=utf-8")
type = DocumentBroker::CLIP_REQUEST_GET_HTML_PLAIN_ONLY;
else
type = DocumentBroker::CLIP_REQUEST_GET;
}
else
{
type = DocumentBroker::CLIP_REQUEST_SET;
ClipboardPartHandler handler;
Poco::Net::HTMLForm form(request, message, handler);
data = handler.getData();
if (!data || data->length() == 0)
LOG_ERR_S("Invalid zero size set clipboard content with tag ["
<< tag << "] on docKey [" << docKey << ']');
}
// Do things in the right thread.
LOG_TRC_S("Move clipboard request tag [" << tag << "] to docbroker thread with "
<< (data ? data->length() : 0)
<< " bytes of data");
docBroker->setupTransfer(
disposition,
[docBroker, type, viewId, tag, data](const std::shared_ptr<Socket>& moveSocket)
{
auto streamSocket = std::static_pointer_cast<StreamSocket>(moveSocket);
docBroker->handleClipboardRequest(type, streamSocket, viewId, tag, data);
});
LOG_TRC_S("queued clipboard command " << type << " on docBroker fetch");
}
// fallback to persistent clipboards if we can
else if (!DocumentBroker::lookupSendClipboardTag(socket, tag, false))
{
LOG_ERR_S("Invalid clipboard request to server ["
<< serverId << "] with tag [" << tag << "] and broker [" << docKey
<< "]: " << (docBroker ? "" : "not ") << "found");
std::string errMsg = "Empty clipboard item / session tag " + tag;
// Bad request.
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket, errMsg);
}
}
void ClientRequestDispatcher::handleRobotsTxtRequest(const Poco::Net::HTTPRequest& request,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG_S("HTTP request: " << request.getURI());
const std::string responseString = "User-agent: *\nDisallow: /\n";
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.set("Content-Length", std::to_string(responseString.size()));
httpResponse.set("Content-Type", "text/plain");
httpResponse.set("Connection", "close");
httpResponse.writeData(socket->getOutBuffer());
if (request.getMethod() == Poco::Net::HTTPRequest::HTTP_GET)
{
socket->send(responseString);
}
socket->shutdown();
LOG_INF_S("Sent robots.txt response successfully");
}
void ClientRequestDispatcher::handleMediaRequest(const Poco::Net::HTTPRequest& request,
SocketDisposition& /*disposition*/,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG_S("Media request: " << request.getURI());
std::string decoded;
Poco::URI::decode(request.getURI(), decoded);
Poco::URI requestUri(decoded);
Poco::URI::QueryParameters params = requestUri.getQueryParameters();
std::string WOPISrc, serverId, viewId, tag, mime;
for (const auto& it : params)
{
if (it.first == "WOPISrc")
WOPISrc = it.second;
else if (it.first == "ServerId")
serverId = it.second;
else if (it.first == "ViewId")
viewId = it.second;
else if (it.first == "Tag")
tag = it.second;
else if (it.first == "MimeType")
mime = it.second;
}
LOG_TRC_S("Media request for us: [" << serverId << "] with tag [" << tag << "] and viewId ["
<< viewId << ']');
if (serverId != Util::getProcessIdentifier())
{
LOG_ERR_S("Cluster configuration error: mis-matching serverid ["
<< serverId << "] vs. [" << Util::getProcessIdentifier()
<< "] on request to URL: " << request.getURI());
// we got the wrong request.
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
// Verify that the WOPISrc is properly encoded.
if (!HttpHelper::verifyWOPISrc(request.getURI(), WOPISrc, socket))
{
return;
}
const auto docKey = RequestDetails::getDocKey(WOPISrc);
LOG_TRC_S("Looking up DocBroker with docKey [" << docKey << "] referenced in WOPISrc ["
<< WOPISrc
<< "] in media URL: " + request.getURI());
std::shared_ptr<DocumentBroker> docBroker;
{
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto it = DocBrokers.find(docKey);
if (it == DocBrokers.end())
{
LOG_ERR_S("Unknown DocBroker with docKey [" << docKey << "] referenced in WOPISrc ["
<< WOPISrc
<< "] in media URL: " + request.getURI());
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
docBroker = it->second;
}
// If we have a valid docBroker, use it.
// Note: there is a race here as DocBroker may
// have already exited its SocketPoll, but we
// haven't cleaned up the DocBrokers container.
// Since we don't care about creating a new one,
// we simply go to the fallback below.
if (docBroker && docBroker->isAlive())
{
// Do things in the right thread.
LOG_TRC_S("Move media request " << tag << " to docbroker thread");
std::string range = request.get("Range", "none");
docBroker->handleMediaRequest(std::move(range), socket, tag);
}
}
std::string ClientRequestDispatcher::getContentType(const std::string& fileName)
{
static std::unordered_map<std::string, std::string> aContentTypes{
{ "svg", "image/svg+xml" },
{ "pot", "application/vnd.ms-powerpoint" },
{ "xla", "application/vnd.ms-excel" },
// Writer documents
{ "sxw", "application/vnd.sun.xml.writer" },
{ "odt", "application/vnd.oasis.opendocument.text" },
{ "fodt", "application/vnd.oasis.opendocument.text-flat-xml" },
// Calc documents
{ "sxc", "application/vnd.sun.xml.calc" },
{ "ods", "application/vnd.oasis.opendocument.spreadsheet" },
{ "fods", "application/vnd.oasis.opendocument.spreadsheet-flat-xml" },
// Impress documents
{ "sxi", "application/vnd.sun.xml.impress" },
{ "odp", "application/vnd.oasis.opendocument.presentation" },
{ "fodp", "application/vnd.oasis.opendocument.presentation-flat-xml" },
// Draw documents
{ "sxd", "application/vnd.sun.xml.draw" },
{ "odg", "application/vnd.oasis.opendocument.graphics" },
{ "fodg", "application/vnd.oasis.opendocument.graphics-flat-xml" },
// Chart documents
{ "odc", "application/vnd.oasis.opendocument.chart" },
// Text master documents
{ "sxg", "application/vnd.sun.xml.writer.global" },
{ "odm", "application/vnd.oasis.opendocument.text-master" },
// Math documents
// In fact Math documents are not supported at all.
// See: https://bugs.documentfoundation.org/show_bug.cgi?id=97006
{ "sxm", "application/vnd.sun.xml.math" },
{ "odf", "application/vnd.oasis.opendocument.formula" },
// Text template documents
{ "stw", "application/vnd.sun.xml.writer.template" },
{ "ott", "application/vnd.oasis.opendocument.text-template" },
// Writer master document templates
{ "otm", "application/vnd.oasis.opendocument.text-master-template" },
// Spreadsheet template documents
{ "stc", "application/vnd.sun.xml.calc.template" },
{ "ots", "application/vnd.oasis.opendocument.spreadsheet-template" },
// Presentation template documents
{ "sti", "application/vnd.sun.xml.impress.template" },
{ "otp", "application/vnd.oasis.opendocument.presentation-template" },
// Drawing template documents
{ "std", "application/vnd.sun.xml.draw.template" },
{ "otg", "application/vnd.oasis.opendocument.graphics-template" },
// MS Word
{ "doc", "application/msword" },
{ "dot", "application/msword" },
// MS Excel
{ "xls", "application/vnd.ms-excel" },
// MS PowerPoint
{ "ppt", "application/vnd.ms-powerpoint" },
// OOXML wordprocessing
{ "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
{ "docm", "application/vnd.ms-word.document.macroEnabled.12" },
{ "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
{ "dotm", "application/vnd.ms-word.template.macroEnabled.12" },
// OOXML spreadsheet
{ "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" },
{ "xltm", "application/vnd.ms-excel.template.macroEnabled.12" },
{ "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
{ "xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" },
{ "xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" },
// OOXML presentation
{ "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
{ "pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" },
{ "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" },
{ "potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" },
// Others
{ "wpd", "application/vnd.wordperfect" },
{ "pdb", "application/x-aportisdoc" },
{ "hwp", "application/x-hwp" },
{ "wps", "application/vnd.ms-works" },
{ "wri", "application/x-mswrite" },
{ "dif", "application/x-dif-document" },
{ "slk", "text/spreadsheet" },
{ "csv", "text/csv" },
{ "dbf", "application/x-dbase" },
{ "wk1", "application/vnd.lotus-1-2-3" },
{ "cgm", "image/cgm" },
{ "dxf", "image/vnd.dxf" },
{ "emf", "image/x-emf" },
{ "wmf", "image/x-wmf" },
{ "cdr", "application/coreldraw" },
{ "vsd", "application/vnd.visio2013" },
{ "vss", "application/vnd.visio" },
{ "pub", "application/x-mspublisher" },
{ "lrf", "application/x-sony-bbeb" },
{ "gnumeric", "application/x-gnumeric" },
{ "mw", "application/macwriteii" },
{ "numbers", "application/x-iwork-numbers-sffnumbers" },
{ "oth", "application/vnd.oasis.opendocument.text-web" },
{ "p65", "application/x-pagemaker" },
{ "rtf", "text/rtf" },
{ "txt", "text/plain" },
{ "fb2", "application/x-fictionbook+xml" },
{ "cwk", "application/clarisworks" },
{ "wpg", "image/x-wpg" },
{ "pages", "application/x-iwork-pages-sffpages" },
{ "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" },
{ "key", "application/x-iwork-keynote-sffkey" },
{ "abw", "application/x-abiword" },
{ "fh", "image/x-freehand" },
{ "sxs", "application/vnd.sun.xml.chart" },
{ "602", "application/x-t602" },
{ "bmp", "image/bmp" },
{ "png", "image/png" },
{ "gif", "image/gif" },
{ "tiff", "image/tiff" },
{ "jpg", "image/jpg" },
{ "jpeg", "image/jpeg" },
{ "pdf", "application/pdf" },
};
const std::string sExt = Poco::Path(fileName).getExtension();
const auto it = aContentTypes.find(sExt);
if (it != aContentTypes.end())
return it->second;
return "application/octet-stream";
}
bool ClientRequestDispatcher::isSpreadsheet(const std::string& fileName)
{
const std::string sContentType = getContentType(fileName);
return sContentType == "application/vnd.oasis.opendocument.spreadsheet" ||
sContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
sContentType == "application/vnd.ms-excel";
}
void ClientRequestDispatcher::handlePostRequest(const RequestDetails& requestDetails,
const Poco::Net::HTTPRequest& request,
Poco::MemoryInputStream& message,
SocketDisposition& disposition,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_INF("Post request: [" << COOLWSD::anonymizeUrl(requestDetails.getURI()) << ']');
if (requestDetails.equals(1, "convert-to") ||
requestDetails.equals(1, "extract-link-targets") ||
requestDetails.equals(1, "get-thumbnail"))
{
// Validate sender - FIXME: should do this even earlier.
if (!allowConvertTo(socket->clientAddress(), request, nullptr))
{
LOG_WRN(
"Conversion requests not allowed from this address: " << socket->clientAddress());
http::Response httpResponse(http::StatusCode::Forbidden);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
ConvertToPartHandler handler;
Poco::Net::HTMLForm form(request, message, handler);
std::string format = (form.has("format") ? form.get("format") : "");
// prefer what is in the URI
if (requestDetails.size() > 2)
format = requestDetails[2];
bool hasRequiredParameters = true;
if (requestDetails.equals(1, "convert-to") && format.empty())
hasRequiredParameters = false;
const std::string fromPath = handler.getFilename();
LOG_INF("Conversion request for URI [" << fromPath << "] format [" << format << "].");
if (!fromPath.empty() && hasRequiredParameters)
{
Poco::URI uriPublic = RequestDetails::sanitizeURI(fromPath);
const std::string docKey = RequestDetails::getDocKey(uriPublic);
std::string options;
if (form.has("options"))
{
// Allow specifying options as-is, in case only data + format are used.
options = form.get("options");
}
const bool fullSheetPreview =
(form.has("FullSheetPreview") && form.get("FullSheetPreview") == "true");
if (fullSheetPreview && format == "pdf" && isSpreadsheet(fromPath))
{
//FIXME: We shouldn't have "true" as having the option already implies that
// we want it enabled (i.e. we shouldn't set the option if we don't want it).
options = ",FullSheetPreview=trueFULLSHEETPREVEND";
}
const std::string pdfVer = (form.has("PDFVer") ? form.get("PDFVer") : "");
if (!pdfVer.empty())
{
if (strcasecmp(pdfVer.c_str(), "PDF/A-1b") &&
strcasecmp(pdfVer.c_str(), "PDF/A-2b") &&
strcasecmp(pdfVer.c_str(), "PDF/A-3b") &&
strcasecmp(pdfVer.c_str(), "PDF-1.5") && strcasecmp(pdfVer.c_str(), "PDF-1.6"))
{
LOG_ERR("Wrong PDF type: " << pdfVer << ". Conversion aborted.");
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
options += ",PDFVer=" + pdfVer + "PDFVEREND";
}
std::string lang = (form.has("lang") ? form.get("lang") : std::string());
std::string target = (form.has("target") ? form.get("target") : std::string());
// This lock could become a bottleneck.
// In that case, we can use a pool and index by publicPath.
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
LOG_DBG("New DocumentBroker for docKey [" << docKey << "].");
auto docBroker = getConvertToBrokerImplementation(
requestDetails[1], fromPath, uriPublic, docKey, format, options, lang, target);
handler.takeFile();
cleanupDocBrokers();
DocBrokers.emplace(docKey, docBroker);
LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after inserting [" << docKey
<< "].");
if (!docBroker->startConversion(disposition, _id))
{
LOG_WRN("Failed to create Client Session with id [" << _id << "] on docKey ["
<< docKey << "].");
cleanupDocBrokers();
}
}
else
{
LOG_INF("Missing parameters for conversion request.");
http::Response httpResponse(http::StatusCode::BadRequest);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
}
return;
}
else if (requestDetails.equals(2, "insertfile"))
{
LOG_INF("Insert file request.");
ConvertToPartHandler handler;
Poco::Net::HTMLForm form(request, message, handler);
if (form.has("childid") && form.has("name"))
{
const std::string formChildid(form.get("childid"));
const std::string formName(form.get("name"));
// Validate the docKey
const std::string decodedUri = requestDetails.getDocumentURI();
const std::string docKey = RequestDetails::getDocKey(decodedUri);
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto docBrokerIt = DocBrokers.find(docKey);
// Maybe just free the client from sending childid in form ?
if (docBrokerIt == DocBrokers.end() || docBrokerIt->second->getJailId() != formChildid)
{
throw BadRequestException("DocKey [" + docKey + "] or childid [" + formChildid +
"] is invalid.");
}
docBrokersLock.unlock();
// protect against attempts to inject something funny here
if (formChildid.find('/') == std::string::npos &&
formName.find('/') == std::string::npos)
{
const std::string dirPath =
COOLWSD::ChildRoot + formChildid + JAILED_DOCUMENT_ROOT + "insertfile";
const std::string fileName = dirPath + '/' + form.get("name");
LOG_INF("Perform insertfile: " << formChildid << ", " << formName
<< ", filename: " << fileName);
Poco::File(dirPath).createDirectories();
Poco::File(handler.getFilename()).moveTo(fileName);
// Cleanup the directory after moving.
const std::string dir = Poco::Path(handler.getFilename()).parent().toString();
if (FileUtil::isEmptyDirectory(dir))
FileUtil::removeFile(dir);
handler.takeFile();
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
socket->ignoreInput();
return;
}
}
}
else if (requestDetails.equals(2, "download"))
{
LOG_INF("File download request.");
// TODO: Check that the user in question has access to this file!
// 1. Validate the dockey
const std::string decodedUri = requestDetails.getDocumentURI();
const std::string docKey = RequestDetails::getDocKey(decodedUri);
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
auto docBrokerIt = DocBrokers.find(docKey);
if (docBrokerIt == DocBrokers.end())
{
throw BadRequestException("DocKey [" + docKey + "] is invalid.");
}
std::string downloadId = requestDetails[3];
std::string url = docBrokerIt->second->getDownloadURL(downloadId);
docBrokerIt->second->unregisterDownloadId(downloadId);
std::string jailId = docBrokerIt->second->getJailId();
docBrokersLock.unlock();
bool foundDownloadId = !url.empty();
std::string decoded;
Poco::URI::decode(url, decoded);
const Poco::Path filePath(COOLWSD::ChildRoot + jailId + JAILED_DOCUMENT_ROOT + decoded);
const std::string filePathAnonym = COOLWSD::anonymizeUrl(filePath.toString());
if (foundDownloadId && filePath.isAbsolute() && Poco::File(filePath).exists())
{
LOG_INF("HTTP request for: " << filePathAnonym);
const std::string& fileName = filePath.getFileName();
const Poco::URI postRequestUri(request.getURI());
const Poco::URI::QueryParameters postRequestQueryParams =
postRequestUri.getQueryParameters();
bool serveAsAttachment = true;
const auto attachmentIt =
std::find_if(postRequestQueryParams.begin(), postRequestQueryParams.end(),
[](const std::pair<std::string, std::string>& element)
{ return element.first == "attachment"; });
if (attachmentIt != postRequestQueryParams.end())
serveAsAttachment = attachmentIt->second != "0";
http::Response response(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(response);
// Instruct browsers to download the file, not display it
// with the exception of SVG where we need the browser to
// actually show it.
const std::string contentType = getContentType(fileName);
response.setContentType(contentType);
if (serveAsAttachment && contentType != "image/svg+xml")
response.set("Content-Disposition", "attachment; filename=\"" + fileName + '"');
#if !MOBILEAPP
if (COOLWSD::WASMState != COOLWSD::WASMActivationState::Disabled)
{
response.add("Cross-Origin-Opener-Policy", "same-origin");
response.add("Cross-Origin-Embedder-Policy", "require-corp");
response.add("Cross-Origin-Resource-Policy", "cross-origin");
}
#endif // !MOBILEAPP
try
{
HttpHelper::sendFileAndShutdown(socket, filePath.toString(), response);
}
catch (const Poco::Exception& exc)
{
LOG_ERR("Error sending file to client: "
<< exc.displayText()
<< (exc.nested() ? " (" + exc.nested()->displayText() + ")" : ""));
}
FileUtil::removeFile(filePath.toString());
}
else
{
if (foundDownloadId)
LOG_ERR("Download file [" << filePathAnonym << "] not found.");
else
LOG_ERR("Download with id [" << downloadId << "] not found.");
http::Response httpResponse(http::StatusCode::NotFound);
httpResponse.set("Content-Length", "0");
socket->sendAndShutdown(httpResponse);
}
return;
}
else if (requestDetails.equals(1, "render-search-result"))
{
RenderSearchResultPartHandler handler;
Poco::Net::HTMLForm form(request, message, handler);
const std::string fromPath = handler.getFilename();
LOG_INF("Create render-search-result POST command handler");
if (fromPath.empty())
return;
Poco::URI uriPublic = RequestDetails::sanitizeURI(fromPath);
const std::string docKey = RequestDetails::getDocKey(uriPublic);
// This lock could become a bottleneck.
// In that case, we can use a pool and index by publicPath.
std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
LOG_DBG("New DocumentBroker for docKey [" << docKey << "].");
auto docBroker = std::make_shared<RenderSearchResultBroker>(
fromPath, uriPublic, docKey, handler.getSearchResultContent());
handler.takeFile();
cleanupDocBrokers();
DocBrokers.emplace(docKey, docBroker);
LOG_TRC("Have " << DocBrokers.size() << " DocBrokers after inserting [" << docKey << "].");
if (!docBroker->executeCommand(disposition, _id))
{
LOG_WRN("Failed to create Client Session with id [" << _id << "] on docKey [" << docKey
<< "].");
cleanupDocBrokers();
}
return;
}
throw BadRequestException("Invalid or unknown request.");
}
void ClientRequestDispatcher::handleClientProxyRequest(const Poco::Net::HTTPRequest& request,
const RequestDetails& requestDetails,
Poco::MemoryInputStream& message,
SocketDisposition& disposition)
{
//FIXME: The DocumentURI includes the WOPISrc, which makes it potentially invalid URI.
const std::string url = requestDetails.getLegacyDocumentURI();
LOG_INF("URL [" << url << "] for Proxy request.");
const auto uriPublic = RequestDetails::sanitizeURI(url);
const auto docKey = RequestDetails::getDocKey(uriPublic);
const std::string fileId = Util::getFilenameFromURL(docKey);
Util::mapAnonymized(fileId, fileId); // Identity mapping, since fileId is already obfuscated
LOG_INF("Starting Proxy request handler for session [" << _id << "] on url ["
<< COOLWSD::anonymizeUrl(url) << "].");
// Check if readonly session is required
bool isReadOnly = false;
for (const auto& param : uriPublic.getQueryParameters())
{
LOG_DBG("Query param: " << param.first << ", value: " << param.second);
if (param.first == "permission" && param.second == "readonly")
{
isReadOnly = true;
}
}
LOG_INF("URL [" << COOLWSD::anonymizeUrl(url) << "] is "
<< (isReadOnly ? "readonly" : "writable") << '.');
(void)request;
(void)message;
(void)disposition;
// Request a kit process for this doc.
std::pair<std::shared_ptr<DocumentBroker>, std::string> pair
= findOrCreateDocBroker(DocumentBroker::ChildType::Interactive, url, docKey, _id, uriPublic,
/*mobileAppDocId=*/0, /*wopiFileInfo=*/nullptr);
auto docBroker = pair.first;
auto errorMsg = pair.second;
if (docBroker)
{
// need to move into the DocumentBroker context before doing session lookup / creation etc.
docBroker->setupTransfer(
disposition,
[docBroker, id = _id, uriPublic, isReadOnly,
requestDetails](const std::shared_ptr<Socket>& moveSocket)
{
// Now inside the document broker thread ...
LOG_TRC_S("In the docbroker thread for " << docBroker->getDocKey());
const int fd = moveSocket->getFD();
auto streamSocket = std::static_pointer_cast<StreamSocket>(moveSocket);
try
{
docBroker->handleProxyRequest(id, uriPublic, isReadOnly, requestDetails,
streamSocket);
return;
}
catch (const UnauthorizedRequestException& exc)
{
LOG_ERR_S("Unauthorized Request while starting session on "
<< docBroker->getDocKey() << " for socket #" << fd
<< ". Terminating connection. Error: " << exc.what());
}
catch (const StorageConnectionException& exc)
{
LOG_ERR_S("Storage error while starting session on "
<< docBroker->getDocKey() << " for socket #" << fd
<< ". Terminating connection. Error: " << exc.what());
}
catch (const std::exception& exc)
{
LOG_ERR_S("Error while starting session on "
<< docBroker->getDocKey() << " for socket #" << fd
<< ". Terminating connection. Error: " << exc.what());
}
// badness occurred:
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, streamSocket);
});
}
else
{
LOG_ERR("Failed to find document [" << docKey << "]: " << errorMsg);
// badness occurred:
auto streamSocket = std::static_pointer_cast<StreamSocket>(disposition.getSocket());
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, streamSocket);
// FIXME: send docunloading & re-try on client ?
}
}
#endif
void ClientRequestDispatcher::handleClientWsUpgrade(const Poco::Net::HTTPRequest& request,
const RequestDetails& requestDetails,
SocketDisposition& disposition,
const std::shared_ptr<StreamSocket>& socket,
unsigned mobileAppDocId)
{
const std::string url = requestDetails.getDocumentURI();
assert(socket && "Must have a valid socket");
// must be trace for anonymization
LOG_TRC("Client WS request: " << requestDetails.getURI() << ", url: " << url << ", socket #"
<< socket->getFD());
// First Upgrade.
auto ws = std::make_shared<WebSocketHandler>(socket, request);
// Response to clients beyond this point is done via WebSocket.
try
{
if (COOLWSD::NumConnections >= COOLWSD::MaxConnections)
{
LOG_INF("Limit on maximum number of connections of " << COOLWSD::MaxConnections
<< " reached.");
if (config::isSupportKeyEnabled())
{
shutdownLimitReached(ws);
return;
}
}
const std::string requestKey = requestDetails.getRequestKey();
if (!requestKey.empty())
{
auto it = RequestVettingStations.find(requestKey);
if (it != RequestVettingStations.end())
{
LOG_TRC("Found RVS under key: " << requestKey);
_rvs = it->second;
RequestVettingStations.erase(it);
}
}
if (!_rvs)
{
LOG_TRC("Creating RVS for key: " << requestKey);
_rvs = std::make_shared<RequestVettingStation>(COOLWSD::getWebServerPoll(),
requestDetails);
}
// Indicate to the client that document broker is searching.
static constexpr const char* const status = "progress: { \"id\":\"find\" }";
LOG_TRC("Sending to Client [" << status << ']');
ws->sendMessage(status);
_rvs->handleRequest(_id, requestDetails, ws, socket, mobileAppDocId, disposition);
}
catch (const std::exception& exc)
{
LOG_ERR("Error while handling Client WS Request: " << exc.what());
const std::string msg = "error: cmd=internal kind=load";
ws->sendMessage(msg);
ws->shutdown(WebSocketHandler::StatusCodes::ENDPOINT_GOING_AWAY, msg);
socket->ignoreInput();
}
}
/// Lookup cached file content.
const std::string& ClientRequestDispatcher::getFileContent(const std::string& filename)
{
const auto it = StaticFileContentCache.find(filename);
if (it == StaticFileContentCache.end())
{
throw Poco::FileAccessDeniedException("Invalid or forbidden file path: [" + filename +
"].");
}
return it->second;
}
/// Process the discovery.xml file and return as string.
std::string ClientRequestDispatcher::getDiscoveryXML()
{
#if MOBILEAPP
// not needed for mobile
return std::string();
#else
std::string discoveryPath =
Poco::Path(Poco::Util::Application::instance().commandPath()).parent().toString() +
"discovery.xml";
if (!Poco::File(discoveryPath).exists())
{
// http://server/hosting/discovery.xml
discoveryPath = COOLWSD::FileServerRoot + "/discovery.xml";
}
const std::string action = "action";
const std::string favIconUrl = "favIconUrl";
const std::string urlsrc = "urlsrc";
const std::string rootUriValue = "%SRV_URI%";
const std::string uriBaseValue = rootUriValue + "/browser/" COOLWSD_VERSION_HASH "/";
const std::string uriValue = uriBaseValue + "cool.html?";
LOG_DBG_S("Processing discovery.xml from " << discoveryPath);
Poco::XML::InputSource inputSrc(discoveryPath);
Poco::XML::DOMParser parser;
Poco::AutoPtr<Poco::XML::Document> docXML = parser.parse(&inputSrc);
Poco::AutoPtr<Poco::XML::NodeList> listNodes = docXML->getElementsByTagName(action);
for (unsigned long it = 0; it < listNodes->length(); ++it)
{
Poco::XML::Element* elem = static_cast<Poco::XML::Element*>(listNodes->item(it));
Poco::XML::Element* parent =
elem->parentNode() ? static_cast<Poco::XML::Element*>(elem->parentNode()) : nullptr;
if (parent && parent->getAttribute("name") == "Capabilities")
{
elem->setAttribute(urlsrc, rootUriValue + CAPABILITIES_END_POINT);
}
else
{
elem->setAttribute(urlsrc, uriValue);
}
// Set the View extensions cache as well.
if (elem->getAttribute("name") == "edit")
{
const std::string ext = elem->getAttribute("ext");
if (COOLWSD::EditFileExtensions.insert(ext).second) // Skip duplicates.
LOG_DBG_S("Enabling editing of [" << ext << "] extension files");
}
else if (elem->getAttribute("name") == "view_comment")
{
const std::string ext = elem->getAttribute("ext");
if (COOLWSD::ViewWithCommentsFileExtensions.insert(ext).second) // Skip duplicates.
LOG_DBG_S("Enabling commenting on [" << ext << "] extension files");
}
}
// turn "images/img.svg" into "http://server.tld/browser/12345abcd/images/img.svg"
listNodes = docXML->getElementsByTagName("app");
for (unsigned long it = 0; it < listNodes->length(); ++it)
{
Poco::XML::Element* elem = static_cast<Poco::XML::Element*>(listNodes->item(it));
if (elem->hasAttribute(favIconUrl))
{
elem->setAttribute(favIconUrl, uriBaseValue + elem->getAttribute(favIconUrl));
}
}
const auto& proofAttribs = GetProofKeyAttributes();
if (!proofAttribs.empty())
{
// Add proof-key element to wopi-discovery root
Poco::AutoPtr<Poco::XML::Element> keyElem = docXML->createElement("proof-key");
for (const auto& attrib : proofAttribs)
keyElem->setAttribute(attrib.first, attrib.second);
docXML->documentElement()->appendChild(keyElem);
}
std::ostringstream ostrXML;
Poco::XML::DOMWriter writer;
writer.writeNode(ostrXML, docXML);
return ostrXML.str();
#endif
}
#if !MOBILEAPP
/// Create the /hosting/capabilities JSON and return as string.
static std::string getCapabilitiesJson(bool convertToAvailable)
{
// Can the convert-to be used?
Poco::JSON::Object::Ptr convert_to = new Poco::JSON::Object;
Poco::Dynamic::Var available = convertToAvailable;
convert_to->set("available", available);
if (available)
convert_to->set("endpoint", "/cool/convert-to");
Poco::JSON::Object::Ptr capabilities = new Poco::JSON::Object;
capabilities->set("convert-to", convert_to);
// Supports the TemplateSaveAs in CheckFileInfo?
// TemplateSaveAs is broken by design, disable it everywhere (and
// remove at some stage too)
capabilities->set("hasTemplateSaveAs", false);
// Supports the TemplateSource in CheckFileInfo?
capabilities->set("hasTemplateSource", true);
// Hint to encourage use on mobile devices
capabilities->set("hasMobileSupport", true);
// Set the product name
capabilities->set("productName", config::getString("product_name", APP_NAME));
// Set the Server ID
capabilities->set("serverId", Util::getProcessIdentifier());
std::string version, hash;
Util::getVersionInfo(version, hash);
// Set the product version
capabilities->set("productVersion", version);
// Set the product version hash
capabilities->set("productVersionHash", hash);
// Set that this is a proxy.php-enabled instance
capabilities->set("hasProxyPrefix", COOLWSD::IsProxyPrefixEnabled);
// Set if this instance supports Zotero
capabilities->set("hasZoteroSupport", config::getBool("zotero.enable", true));
// Set if this instance supports WASM.
capabilities->set("hasWASMSupport",
COOLWSD::WASMState != COOLWSD::WASMActivationState::Disabled);
std::ostringstream ostrJSON;
capabilities->stringify(ostrJSON);
return ostrJSON.str();
}
cool#8465 clipboard: improve handling of plain text copy, complex case In case the selection is complex (not simple), we used to just request HTML, and then the browser converted that to plain text, which has the downsides already mentioned in commit 7f9de46688a64b42ba8f65cceb9fe2c6ddab89ef (cool#8465 clipboard: improve handling of plain text copy, simple case, 2024-03-08). Steps to support this: 1) Clipboard.js now asks for the text/html,text/plain;charset=utf-8 MIME types. 2) wsd: ClientRequestDispatcher::handleClipboardRequest() now maps this to DocumentBroker::CLIP_REQUEST_GET_HTML_PLAIN_ONLY 3) ClientSession::handleClipboardRequest() maps this to the HTML+plain text MIME type list. 4) kit: ChildSession::getClipboard() is now improved to take a list of MIME types, not just 1 or everything. 5) kit: ChildSession::getClipboard() now emits JSON in case not all, but multiple MIME types are requested. 6) wsd: ClientSession::postProcessCopyPayload() now knows how to postprocess clipboardcontent messages, which may or may not be JSON (it's JSON if more formats are requested explicitly, leaving the 1 format or all format cases unchanged) 7) Control.DownloadProgress.js now handles the case when we get JSON and sets the core-provided plain text next to the HTML. Leave the handling of non-JSON case in, because this means we can copy from an old COOL server to a new one. Note that this approach has the benefit that once the clipboard marker is inserted, the length of the text/html format would change, which means we can't parse the clipboard data till the marker is removed. Emitting JSON for html+text means adding the marker keeps the ability to parse the HTML and the plain text part of the clipboard in JS. Signed-off-by: Miklos Vajna <vmiklos@collabora.com> Change-Id: I67a1f669e8a638d34cc25a2f288a7b30884b9892
2024-03-19 12:12:16 +01:00
/// Send the /hosting/capabilities JSON to socket
static void sendCapabilities(bool convertToAvailable,
const std::shared_ptr<StreamSocket>& socket)
{
std::string capabilities = getCapabilitiesJson(convertToAvailable);
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.setBody(capabilities, "application/json");
httpResponse.set("X-Content-Type-Options", "nosniff");
socket->sendAndShutdown(httpResponse);
LOG_INF("Sent capabilities.json successfully.");
}
void ClientRequestDispatcher::handleCapabilitiesRequest(const Poco::Net::HTTPRequest& request,
const std::shared_ptr<StreamSocket>& socket)
{
assert(socket && "Must have a valid socket");
LOG_DBG("Wopi capabilities request: " << request.getURI());
AsyncFn convertToAllowedCb = [socket](bool allowedConvert){
COOLWSD::getWebServerPoll()->addCallback([socket, allowedConvert]() { sendCapabilities(allowedConvert, socket); });
};
allowConvertTo(socket->clientAddress(), request, convertToAllowedCb);
}
#endif
cool#8465 clipboard: improve handling of plain text copy, complex case In case the selection is complex (not simple), we used to just request HTML, and then the browser converted that to plain text, which has the downsides already mentioned in commit 7f9de46688a64b42ba8f65cceb9fe2c6ddab89ef (cool#8465 clipboard: improve handling of plain text copy, simple case, 2024-03-08). Steps to support this: 1) Clipboard.js now asks for the text/html,text/plain;charset=utf-8 MIME types. 2) wsd: ClientRequestDispatcher::handleClipboardRequest() now maps this to DocumentBroker::CLIP_REQUEST_GET_HTML_PLAIN_ONLY 3) ClientSession::handleClipboardRequest() maps this to the HTML+plain text MIME type list. 4) kit: ChildSession::getClipboard() is now improved to take a list of MIME types, not just 1 or everything. 5) kit: ChildSession::getClipboard() now emits JSON in case not all, but multiple MIME types are requested. 6) wsd: ClientSession::postProcessCopyPayload() now knows how to postprocess clipboardcontent messages, which may or may not be JSON (it's JSON if more formats are requested explicitly, leaving the 1 format or all format cases unchanged) 7) Control.DownloadProgress.js now handles the case when we get JSON and sets the core-provided plain text next to the HTML. Leave the handling of non-JSON case in, because this means we can copy from an old COOL server to a new one. Note that this approach has the benefit that once the clipboard marker is inserted, the length of the text/html format would change, which means we can't parse the clipboard data till the marker is removed. Emitting JSON for html+text means adding the marker keeps the ability to parse the HTML and the plain text part of the clipboard in JS. Signed-off-by: Miklos Vajna <vmiklos@collabora.com> Change-Id: I67a1f669e8a638d34cc25a2f288a7b30884b9892
2024-03-19 12:12:16 +01:00
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */