collabora-online/wsd/ClientSession.cpp

2862 lines
106 KiB
C++
Raw Permalink 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 "ClientSession.hpp"
#include <ios>
#include <sstream>
#include <string>
#include <string_view>
#include <memory>
#include <unordered_map>
#include <Poco/Base64Decoder.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/StreamCopier.h>
#include <Poco/URI.h>
#include "DocumentBroker.hpp"
#include "COOLWSD.hpp"
#include "FileServer.hpp"
#include <common/Common.hpp>
#include <common/JsonUtil.hpp>
#include <common/Log.hpp>
#include <common/Protocol.hpp>
#include <common/Clipboard.hpp>
#include <common/Session.hpp>
#include <common/TraceEvent.hpp>
#include <common/Util.hpp>
#include <common/CommandControl.hpp>
#if !MOBILEAPP
#include <net/HttpHelper.hpp>
#endif
using namespace COOLProtocol;
static constexpr float TILES_ON_FLY_MIN_UPPER_LIMIT = 10.0;
static constexpr int SYNTHETIC_COOL_PID_OFFSET = 10000000;
using Poco::Path;
// rotates regularly
const int ClipboardTokenLengthBytes = 16;
// home-use, disabled by default.
const int ProxyAccessTokenLengthBytes = 32;
static std::mutex GlobalSessionMapMutex;
static std::unordered_map<std::string, std::weak_ptr<ClientSession>> GlobalSessionMap;
namespace
{
void logSyntaxErrorDetails(const StringVector& tokens, const std::string& firstLine)
{
LOG_WRN("Invalid syntax for '" << tokens[0] << "' message: [" << firstLine << ']');
}
}
ClientSession::ClientSession(
const std::shared_ptr<ProtocolHandlerInterface>& ws,
const std::string& id,
const std::shared_ptr<DocumentBroker>& docBroker,
const Poco::URI& uriPublic,
const bool readOnly,
const RequestDetails &requestDetails) :
Session(ws, "ToClient-" + id, id, readOnly),
_docBroker(docBroker),
_uriPublic(uriPublic),
_auth(Authorization::create(uriPublic)),
_isDocumentOwner(false),
_state(SessionState::DETACHED),
_lastStateTime(std::chrono::steady_clock::now()),
_keyEvents(1),
_clientVisibleArea(0, 0, 0, 0),
_splitX(0),
_splitY(0),
_clientSelectedPart(-1),
_clientSelectedMode(0),
_tileWidthPixel(0),
_tileHeightPixel(0),
_tileWidthTwips(0),
_tileHeightTwips(0),
_kitViewId(-1),
_serverURL(requestDetails),
_isTextDocument(false),
_thumbnailSession(false),
_canonicalViewId(0)
{
const std::size_t curConnections = ++COOLWSD::NumConnections;
LOG_INF("ClientSession ctor [" << getName() << "] for URI: [" << _uriPublic.toString()
<< "], current number of connections: " << curConnections);
// populate with random values.
for (size_t i = 0; i < N_ELEMENTS(_clipboardKeys); ++i)
rotateClipboardKey(false);
// Emit metadata Trace Events for the synthetic pid used for the Trace Events coming in from the
// client's cool, and for its dummy thread.
TraceEvent::emitOneRecordingIfEnabled("{\"name\":\"process_name\",\"ph\":\"M\",\"args\":{\"name\":\""
"cool-" + id
+ "\"},\"pid\":"
+ std::to_string(getpid() + SYNTHETIC_COOL_PID_OFFSET)
+ ",\"tid\":1},\n");
TraceEvent::emitOneRecordingIfEnabled("{\"name\":\"thread_name\",\"ph\":\"M\",\"args\":{\"name\":\"JS\"},\"pid\":"
+ std::to_string(getpid() + SYNTHETIC_COOL_PID_OFFSET)
+ ",\"tid\":1},\n");
}
// Can't take a reference in the constructor.
void ClientSession::construct()
{
std::unique_lock<std::mutex> lock(GlobalSessionMapMutex);
MessageHandlerInterface::initialize();
GlobalSessionMap[getId()] = client_from_this();
}
ClientSession::~ClientSession()
{
const std::size_t curConnections = --COOLWSD::NumConnections;
LOG_INF("~ClientSession dtor [" << getName() << "], current number of connections: " << curConnections);
std::unique_lock<std::mutex> lock(GlobalSessionMapMutex);
GlobalSessionMap.erase(getId());
}
void ClientSession::setState(SessionState newState)
{
LOG_TRC("transition from " << name(_state) << " to " << name(newState));
// we can get incoming messages while our disconnection is in transit.
if (_state == SessionState::WAIT_DISCONNECT)
{
if (newState != SessionState::WAIT_DISCONNECT)
LOG_WRN("Unusual race - attempts to transition from " << name(_state) << " to "
<< name(newState));
return;
}
switch (newState)
{
case SessionState::DETACHED:
assert(_state == SessionState::DETACHED);
break;
case SessionState::LOADING:
assert(_state == SessionState::DETACHED);
break;
case SessionState::LIVE:
assert(_state == SessionState::LIVE ||
_state == SessionState::LOADING);
break;
case SessionState::WAIT_DISCONNECT:
assert(_state == SessionState::LOADING ||
_state == SessionState::LIVE);
break;
}
_state = newState;
_lastStateTime = std::chrono::steady_clock::now();
}
bool ClientSession::disconnectFromKit()
{
assert(_state != SessionState::WAIT_DISCONNECT);
auto docBroker = getDocumentBroker();
if (docBroker && (_state == SessionState::LIVE || _state == SessionState::LOADING))
{
setState(SessionState::WAIT_DISCONNECT);
// handshake nicely; so wait for 'disconnected'
LOG_TRC("Sending 'disconnect' command to session " << getId());
docBroker->forwardToChild(client_from_this(), "disconnect");
return false;
}
return true; // just get on with it
}
// Allow 20secs for the clipboard and disconnection to come.
bool ClientSession::staleWaitDisconnect(const std::chrono::steady_clock::time_point &now)
{
if (_state != SessionState::WAIT_DISCONNECT)
return false;
return std::chrono::duration_cast<std::chrono::seconds>(now - _lastStateTime).count() >= 20;
}
void ClientSession::rotateClipboardKey(bool notifyClient)
{
if (_state == SessionState::WAIT_DISCONNECT)
return;
_clipboardKeys[1] = _clipboardKeys[0];
_clipboardKeys[0] = Util::rng::getHexString(
ClipboardTokenLengthBytes);
LOG_TRC("Clipboard key on [" << getId() << "] set to " << _clipboardKeys[0] <<
" last was " << _clipboardKeys[1]);
if (notifyClient)
sendTextFrame("clipboardkey: " + _clipboardKeys[0]);
}
std::string ClientSession::getClipboardURI(bool encode)
{
if (_wopiFileInfo && _wopiFileInfo->getDisableCopy())
return std::string();
return createPublicURI("clipboard", _clipboardKeys[0], encode);
}
std::string ClientSession::createPublicURI(const std::string& subPath, const std::string& tag, bool encode)
{
Poco::URI wopiSrc = getDocumentBroker()->getPublicUri();
wopiSrc.setQueryParameters(Poco::URI::QueryParameters());
const std::string encodedFrom = Util::encodeURIComponent(wopiSrc.toString());
std::string meta = _serverURL.getSubURLForEndpoint(
"/cool/" + subPath + "?WOPISrc=" + encodedFrom +
"&ServerId=" + Util::getProcessIdentifier() +
"&ViewId=" + std::to_string(getKitViewId()) +
"&Tag=" + tag);
#if !MOBILEAPP
if (!COOLWSD::RouteToken.empty())
meta += "&RouteToken=" + COOLWSD::RouteToken;
#endif
if (!encode)
return meta;
return Util::encodeURIComponent(meta);
}
bool ClientSession::matchesClipboardKeys(const std::string &/*viewId*/, const std::string &tag)
{
if (tag.empty())
{
LOG_ERR("Invalid, empty clipboard tag");
return false;
}
// FIXME: check viewId for paranoia if we can.
return std::any_of(std::begin(_clipboardKeys), std::end(_clipboardKeys),
[&tag](const std::string& it) { return it == tag; });
}
void ClientSession::handleClipboardRequest(DocumentBroker::ClipboardRequest type,
const std::shared_ptr<StreamSocket> &socket,
const std::string &tag,
const std::shared_ptr<std::string> &data)
{
// Move the socket into our DocBroker.
auto docBroker = getDocumentBroker();
docBroker->addSocketToPoll(socket);
if (_state == SessionState::WAIT_DISCONNECT)
{
LOG_TRC("Clipboard request " << tag << " for disconnecting session");
if (docBroker->lookupSendClipboardTag(socket, tag, false))
return; // the getclipboard already completed.
if (type == DocumentBroker::CLIP_REQUEST_SET)
{
#if !MOBILEAPP
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
#endif
}
else // will be handled during shutdown
{
LOG_TRC("Clipboard request " << tag << " queued for shutdown");
_clipSockets.push_back(socket);
}
}
std::string specific;
if (type == DocumentBroker::CLIP_REQUEST_GET_RICH_HTML_ONLY)
specific = " text/html";
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 (type == DocumentBroker::CLIP_REQUEST_GET_HTML_PLAIN_ONLY)
{
specific = " text/html,text/plain;charset=utf-8";
}
if (type != DocumentBroker::CLIP_REQUEST_SET)
{
if (_wopiFileInfo && _wopiFileInfo->getDisableCopy())
{
// Unsupported clipboard request.
LOG_ERR("Unsupported Clipboard Request from socket #" << socket->getFD()
<< ". Terminating connection.");
std::ostringstream oss;
oss << "HTTP/1.1 403 Forbidden\r\n"
<< "Date: " << Util::getHttpTimeNow() << "\r\n"
<< "User-Agent: " << http::getAgentString() << "\r\n"
<< "Content-Length: 0\r\n"
<< "Connection: close\r\n"
<< "\r\n";
socket->send(oss.str());
socket->closeConnection(); // Shutdown socket.
socket->ignoreInput();
return;
}
LOG_TRC("Session [" << getId() << "] sending getclipboard" + specific);
docBroker->forwardToChild(client_from_this(), "getclipboard" + specific);
_clipSockets.push_back(socket);
}
else // REQUEST_SET
{
// FIXME: manage memory more efficiently.
LOG_TRC("Session [" << getId() << "] sending setclipboard");
if (data.get())
{
clipboard: fix partial content read for text/html Problem: In ClientSession::postProcessCopyPayload() the serialized form of clipboard's text/html is modified to inject meta origin and sent to the client. When this data is sent back from client to the server(possibly a different one) to set the core clipboard via "setclipboard", the parser gets confused because the 'text/html' part/flavour's data length as originally set by the core the first time does not agree with the actual length due to the meta origin injection done previously. As a result the parsed results of 'text/html' is only partial and possibly the parsing of subsequent flavours are affected. Fix: Add a preprocess step for the payload for setclipboard to remove the meta origin tag from the payload before parsing is done (in ChildSession). The original clipboard payload is created in ChildSession::getClipboard(). Note that here we encode every flavour core supports ('text/html' is just one of them) where the type and size of each flavour data is included. The problem is that later on, ClientSession::postProcessCopyPayload() injects a meta tag(which is needed in the client) into this multi favour payload without adjusting the size of the flavour (which messes up any subsequent parsing of the 'text/html' flavour section and those after it). We could adjust/correct the size in ClientSession::postProcessCopyPayload(), which is one solution but the way we do it here (string search on the whole payload!) without proper parsing seems less futuristic (things can go wrong if we change the format of packing flavour data in the payload). I feel that a less hacky solution is to remove the meta tag (current patch) to make it exactly how it was created in ChildSession::getClipboard() so that we don't have to do any error-prone size adjustments in ClientSession::postProcessCopyPayload() or when parsing the payload. Signed-off-by: Dennis Francis <dennis.francis@collabora.com> Change-Id: I285f133829ea0f2dd1cd07d17e90f794ba9a6caf
2021-10-22 09:20:19 +02:00
preProcessSetClipboardPayload(*data);
docBroker->forwardToChild(client_from_this(), "setclipboard\n" + *data, true);
// FIXME: work harder for error detection ?
std::ostringstream oss;
oss << "HTTP/1.1 200 OK\r\n"
<< "Date: " << Util::getHttpTimeNow() << "\r\n"
<< "User-Agent: " << http::getAgentString() << "\r\n"
<< "Content-Length: 0\r\n"
<< "Connection: close\r\n"
<< "\r\n";
socket->send(oss.str());
socket->shutdown();
}
else
{
#if !MOBILEAPP
HttpHelper::sendErrorAndShutdown(http::StatusCode::BadRequest, socket);
#endif
}
}
}
void ClientSession::onTileProcessed(TileWireId wireId)
{
auto iter = std::find_if(_tilesOnFly.begin(), _tilesOnFly.end(),
[wireId](const std::pair<TileWireId, std::chrono::steady_clock::time_point>& curTile)
{
return curTile.first == wireId;
});
if(iter != _tilesOnFly.end())
_tilesOnFly.erase(iter);
else
LOG_INF("Tileprocessed message with an unknown wire-id '" << wireId << "' from session " << getId());
}
bool ClientSession::_handleInput(const char *buffer, int length)
{
LOG_TRC("handling incoming [" << getAbbreviatedMessage(buffer, length) << ']');
const std::string firstLine = getFirstLine(buffer, length);
const StringVector tokens = StringVector::tokenize(firstLine.data(), firstLine.size());
std::shared_ptr<DocumentBroker> docBroker = getDocumentBroker();
if (!docBroker || docBroker->isMarkedToDestroy())
{
LOG_ERR("No DocBroker found, or DocBroker marked to be destroyed. Terminating session " << getName());
return false;
}
if (tokens.size() < 1)
{
sendTextFrameAndLogError("error: cmd=empty kind=unknown");
return false;
}
if (tokens.equals(0, "DEBUG"))
{
LOG_DBG("From client: " << std::string(buffer, length).substr(strlen("DEBUG") + 1));
return false;
}
else if (tokens.equals(0, "ERROR"))
{
LOG_ERR("From client: " << std::string(buffer, length).substr(strlen("ERROR") + 1));
return false;
}
else if (tokens.equals(0, "TRACEEVENT"))
{
if (COOLWSD::EnableTraceEventLogging)
{
if (_performanceCounterEpoch == 0)
{
static bool warnedOnce = false;
if (!warnedOnce)
{
LOG_WRN("For some reason the _performanceCounterEpoch is still zero, ignoring TRACEEVENT from cool as the timestamp would be garbage");
warnedOnce = true;
}
return false;
} else if (_performanceCounterEpoch < 1620000000000000ull || _performanceCounterEpoch > 2000000000000000ull)
{
static bool warnedOnce = false;
if (!warnedOnce)
{
LOG_WRN("For some reason the _performanceCounterEpoch is bogus, ignoring TRACEEVENT from cool as the timestamp would be garbage");
warnedOnce = true;
}
return false;
}
if (tokens.size() >= 4)
{
// The intent is that when doing Trace Event generation, the web browser client and
// the server run on the same machine, so there is no clock skew problem.
std::string name;
std::string ph;
uint64_t ts;
if (getTokenString(tokens[1], "name", name) &&
getTokenString(tokens[2], "ph", ph) &&
getTokenUInt64(tokens[3], "ts", ts))
{
std::string args;
if (tokens.size() >= 5 && getTokenString(tokens, "args", args))
args = ",\"args\":" + args;
uint64_t id, tid;
uint64_t dur;
if (ph == "i")
{
COOLWSD::writeTraceEventRecording("{\"name\":"
+ name
+ ",\"ph\":\"i\""
+ args
+ ",\"ts\":"
+ std::to_string(ts + _performanceCounterEpoch)
+ ",\"pid\":"
+ std::to_string(getpid() + SYNTHETIC_COOL_PID_OFFSET)
+ ",\"tid\":1},\n");
}
// Should the first getTokenUInt64()'s return value really
// be ignored?
else if ((ph == "S" || ph == "F") &&
(static_cast<void>(getTokenUInt64(tokens[4], "id", id)),
getTokenUInt64(tokens[5], "tid", tid)))
{
COOLWSD::writeTraceEventRecording("{\"name\":"
+ name
+ ",\"ph\":\""
+ ph
+ "\""
+ args
+ ",\"ts\":"
+ std::to_string(ts + _performanceCounterEpoch)
+ ",\"pid\":"
+ std::to_string(getpid() + SYNTHETIC_COOL_PID_OFFSET)
+ ",\"tid\":"
+ std::to_string(tid)
+ ",\"id\":"
+ std::to_string(id)
+ "},\n");
}
else if (ph == "X" &&
getTokenUInt64(tokens[4], "dur", dur))
{
COOLWSD::writeTraceEventRecording("{\"name\":"
+ name
+ ",\"ph\":\"X\""
+ args
+ ",\"ts\":"
+ std::to_string(ts + _performanceCounterEpoch)
+ ",\"pid\":"
+ std::to_string(getpid() + SYNTHETIC_COOL_PID_OFFSET)
+ ",\"tid\":1"
",\"dur\":"
+ std::to_string(dur)
+ "},\n");
}
else
{
LOG_WRN("Unrecognized TRACEEVENT message");
}
}
}
else
LOG_WRN("Unrecognized TRACEEVENT message");
}
return false;
}
COOLWSD::dumpIncomingTrace(docBroker->getJailId(), getId(), firstLine);
if (COOLProtocol::tokenIndicatesUserInteraction(tokens[0]))
{
// Keep track of timestamps of incoming client messages that indicate user activity.
updateLastActivityTime();
docBroker->updateLastActivityTime();
if (isEditable() && isViewLoaded())
{
assert(!inWaitDisconnected() && "A writable view can't be waiting disconnection.");
docBroker->updateEditingSessionId(getId());
}
}
if (tokens.equals(0, "urp"))
{
// This can't be pushed down into the long list of tokens that are
// forwarded to the child later as we need it to be able to run before
// documents are loaded
LOG_TRC("UNO remote protocol message (from client): " << firstLine);
return forwardToChild(std::string(buffer, length), docBroker);
}
if (tokens.equals(0, "coolclient"))
{
if (tokens.size() < 2)
{
sendTextFrameAndLogError("error: cmd=coolclient kind=badprotocolversion");
return false;
}
const std::tuple<int, int, std::string> versionTuple = ParseVersion(tokens[1]);
if (std::get<0>(versionTuple) != ProtocolMajorVersionNumber ||
std::get<1>(versionTuple) != ProtocolMinorVersionNumber)
{
sendTextFrameAndLogError("error: cmd=coolclient kind=badprotocolversion");
return false;
}
_performanceCounterEpoch = 0;
if (tokens.size() >= 4)
{
const std::string timestamp = tokens[2];
const char* str = timestamp.c_str();
char* endptr = nullptr;
uint64_t ts = strtoull(str, &endptr, 10);
if (*endptr == '\0')
{
const std::string perfcounter = tokens[3];
str = perfcounter.data();
endptr = nullptr;
double counter = strtod(str, &endptr);
if (*endptr == '\0' && counter > 0 &&
(counter < (double)(uint64_t)(std::numeric_limits<uint64_t>::max() / 1000)))
{
// Now we know how to translate from the client's performance.now() values to
// microseconds since the epoch.
_performanceCounterEpoch = ts * 1000 - (uint64_t)(counter * 1000);
LOG_INF("Client timestamps: Date.now():" << ts <<
", performance.now():" << counter
<< " => " << _performanceCounterEpoch);
}
}
}
// Send COOL version information
sendTextFrame("coolserver " + Util::getVersionJSON(EnableExperimental));
// Send LOKit version information
sendTextFrame("lokitversion " + COOLWSD::LOKitVersion);
// If Trace Event generation and logging is enabled (whether it can be turned on), tell it
// to cool
if (COOLWSD::EnableTraceEventLogging)
sendTextFrame("enabletraceeventlogging yes");
if (!Util::isMobileApp())
{
// If it is not mobile, it must be Linux (for now).
std::string osVersionInfo(COOLWSD::getConfigValue<std::string>("per_view.custom_os_info", ""));
if (osVersionInfo.empty())
osVersionInfo = Util::getLinuxVersion();
sendTextFrame(std::string("osinfo ") + osVersionInfo);
}
// Send clipboard key
rotateClipboardKey(true);
return true;
}
if (tokens.equals(0, "versionbar"))
{
#if !MOBILEAPP
std::string versionBar;
{
std::lock_guard<std::mutex> lock(COOLWSD::FetchUpdateMutex);
versionBar = COOLWSD::LatestVersion;
}
if (!versionBar.empty())
sendTextFrame("versionbar: " + versionBar);
#endif
}
else if (tokens.equals(0, "jserror") || tokens.equals(0, "jsexception"))
{
LOG_ERR(std::string(buffer, length));
return true;
}
else if (tokens.equals(0, "load"))
{
if (!getDocURL().empty())
{
sendTextFrameAndLogError("error: cmd=load kind=docalreadyloaded");
return false;
}
return loadDocument(buffer, length, tokens, docBroker);
}
else if (tokens.equals(0, "loadwithpassword"))
{
std::string docPassword;
if (tokens.size() > 1 && getTokenString(tokens[1], "password", docPassword))
{
if (!docPassword.empty())
{
setHaveDocPassword(true);
setDocPassword(docPassword);
}
}
return loadDocument(buffer, length, tokens, docBroker);
}
else if (getDocURL().empty())
{
sendTextFrameAndLogError("error: cmd=" + tokens[0] + " kind=nodocloaded");
return false;
}
else if (tokens.equals(0, "commandvalues"))
{
return getCommandValues(buffer, length, tokens, docBroker);
}
else if (tokens.equals(0, "closedocument"))
{
// If this session is the owner of the file & 'EnableOwnerTermination' feature
// is turned on by WOPI, let it close all sessions
if (isDocumentOwner() && _wopiFileInfo && _wopiFileInfo->getEnableOwnerTermination())
{
LOG_DBG("Session [" << getId() << "] requested owner termination");
docBroker->closeDocument("ownertermination");
}
else if (docBroker->isDocumentChangedInStorage())
{
LOG_DBG("Document marked as changed in storage and user ["
<< getUserId() << ", " << getUserName()
<< "] wants to refresh the document for all.");
docBroker->stop("documentconflict " + getUserName());
}
return true;
}
else if (tokens.equals(0, "versionrestore"))
{
if (tokens.size() > 1 && tokens.equals(1, "prerestore"))
{
// green signal to WOPI host to restore the version *after* saving
// any unsaved changes, if any, to the storage
docBroker->closeDocument("versionrestore: prerestore_ack");
}
}
else if (tokens.equals(0, "partpagerectangles"))
{
// We don't support partpagerectangles any more, will be removed in the
// next version
sendTextFrame("partpagerectangles: ");
return true;
}
else if (tokens.equals(0, "ping"))
{
std::string count = std::to_string(docBroker->getRenderedTileCount());
sendTextFrame("pong rendercount=" + count);
return true;
}
else if (tokens.equals(0, "renderfont"))
{
return sendFontRendering(buffer, length, tokens, docBroker);
}
else if (tokens.equals(0, "status") || tokens.equals(0, "statusupdate"))
{
assert(firstLine.size() == static_cast<std::size_t>(length));
return forwardToChild(firstLine, docBroker);
}
else if (tokens.equals(0, "tile"))
{
if (!(UnitWSD::isUnitTesting() ? true : getCanonicalViewId() != 0 && getCanonicalViewId() >= 1000))
{
LOG_WRN("Got tile request for session [" << getId() << "] on document [" << docBroker->getDocKey()
<< "] with invalid view ID [" << getCanonicalViewId() << "].");
}
return sendTile(buffer, length, tokens, docBroker);
}
else if (tokens.equals(0, "tilecombine"))
{
if (!(UnitWSD::isUnitTesting() ? true : getCanonicalViewId() != 0 && getCanonicalViewId() >= 1000))
{
LOG_WRN("Got tilecombine request for session [" << getId() << "] on document [" << docBroker->getDocKey()
<< "] with invalid view ID [" << getCanonicalViewId() << "].");
}
return sendCombinedTiles(buffer, length, tokens, docBroker);
}
else if (tokens.equals(0, "save"))
{
// If we can't write to Storage, there is no point in saving.
if (!isWritable())
{
LOG_WRN("Session [" << getId() << "] on document [" << docBroker->getDocKey()
<< "] has no write permissions in Storage and cannot save.");
sendTextFrameAndLogError("error: cmd=save kind=savefailed");
}
else
{
// Don't save unmodified docs by default.
int dontSaveIfUnmodified = 1;
int dontTerminateEdit = 1;
std::string extendedData;
// We expect at most 3 arguments.
for (int i = 0; i < 3; ++i)
{
// +1 to skip the command token.
const StringVector attr = StringVector::tokenize(tokens[i + 1], '=');
if (attr.size() == 2)
{
if (attr[0] == "dontTerminateEdit")
COOLProtocol::stringToInteger(attr[1], dontTerminateEdit);
else if (attr[0] == "dontSaveIfUnmodified")
COOLProtocol::stringToInteger(attr[1], dontSaveIfUnmodified);
else if (attr[0] == "extendedData")
{
std::string decoded;
Poco::URI::decode(attr[1], decoded);
extendedData = decoded;
}
}
}
docBroker->manualSave(client_from_this(), dontTerminateEdit != 0,
dontSaveIfUnmodified != 0, extendedData);
}
}
else if (tokens.equals(0, "savetostorage"))
{
// By default savetostorage implies forcing.
int force = 1;
if (tokens.size() > 1)
(void)getTokenInteger(tokens[1], "force", force);
// The savetostorage command is really only used to resolve save conflicts
// and it seems to always have force=1. However, we should still honor the
// contract and do as told, not as we expect the API to be used. Use force if provided.
docBroker->uploadToStorage(client_from_this(), force);
}
else if (tokens.equals(0, "clientvisiblearea"))
{
int x;
int y;
int width;
int height;
if ((tokens.size() != 5 && tokens.size() != 7) ||
!getTokenInteger(tokens[1], "x", x) ||
!getTokenInteger(tokens[2], "y", y) ||
!getTokenInteger(tokens[3], "width", width) ||
!getTokenInteger(tokens[4], "height", height))
{
// Be forgiving and log instead of disconnecting.
// sendTextFrameAndLogError("error: cmd=clientvisiblearea kind=syntax");
logSyntaxErrorDetails(tokens, firstLine);
return true;
}
else
{
if (tokens.size() == 7)
{
int splitX, splitY;
if (!getTokenInteger(tokens[5], "splitx", splitX) ||
!getTokenInteger(tokens[6], "splity", splitY))
{
logSyntaxErrorDetails(tokens, firstLine);
return true;
}
_splitX = splitX;
_splitY = splitY;
}
// Untrusted user input, make sure these are not negative.
if (width < 0)
{
width = 0;
}
if (height < 0)
{
height = 0;
}
_clientVisibleArea = Util::Rectangle(x, y, width, height);
return forwardToChild(std::string(buffer, length), docBroker);
}
}
else if (tokens.equals(0, "setclientpart"))
{
if(!_isTextDocument)
{
int temp;
if (tokens.size() != 2 ||
!getTokenInteger(tokens[1], "part", temp))
{
logSyntaxErrorDetails(tokens, firstLine);
return false;
}
else
{
_clientSelectedPart = temp;
return forwardToChild(std::string(buffer, length), docBroker);
}
}
}
else if (tokens.equals(0, "selectclientpart"))
{
if(!_isTextDocument)
{
int part;
int how;
if (tokens.size() != 3 ||
!getTokenInteger(tokens[1], "part", part) ||
!getTokenInteger(tokens[2], "how", how))
{
sendTextFrameAndLogError("error: cmd=selectclientpart kind=syntax");
return false;
}
else
{
return forwardToChild(std::string(buffer, length), docBroker);
}
}
}
else if (tokens.equals(0, "moveselectedclientparts"))
{
if (!_isTextDocument)
{
int nPosition;
if (tokens.size() != 2 ||
!getTokenInteger(tokens[1], "position", nPosition))
{
sendTextFrameAndLogError("error: cmd=moveselectedclientparts kind=syntax");
return false;
}
else
{
if (isEditable())
docBroker->updateLastModifyingActivityTime();
return forwardToChild(std::string(buffer, length), docBroker);
}
}
}
else if (tokens.equals(0, "clientzoom"))
{
int tilePixelWidth, tilePixelHeight, tileTwipWidth, tileTwipHeight;
if (tokens.size() != 5 ||
!getTokenInteger(tokens[1], "tilepixelwidth", tilePixelWidth) ||
!getTokenInteger(tokens[2], "tilepixelheight", tilePixelHeight) ||
!getTokenInteger(tokens[3], "tiletwipwidth", tileTwipWidth) ||
!getTokenInteger(tokens[4], "tiletwipheight", tileTwipHeight))
{
// Be forgiving and log instead of disconnecting.
// sendTextFrameAndLogError("error: cmd=clientzoom kind=syntax");
logSyntaxErrorDetails(tokens, firstLine);
return true;
}
else
{
_tileWidthPixel = tilePixelWidth;
_tileHeightPixel = tilePixelHeight;
_tileWidthTwips = tileTwipWidth;
_tileHeightTwips = tileTwipHeight;
return forwardToChild(std::string(buffer, length), docBroker);
}
}
else if (tokens.equals(0, "tileprocessed"))
{
std::string wids;
if (tokens.size() != 2 ||
!getTokenString(tokens[1], "wids", wids))
{
// Be forgiving and log instead of disconnecting.
// sendTextFrameAndLogError("error: cmd=tileprocessed kind=syntax");
logSyntaxErrorDetails(tokens, firstLine);
return true;
}
// call onTileProcessed on each tileID of tileid1, tileid2, ...
auto lambda = [this](size_t /*nIndex*/, const std::string_view token){
std::string copy(token);
TileWireId wireId = 0; bool res;
std::tie(wireId, res) = Util::i32FromString(copy);
if (!res)
LOG_WRN("Invalid syntax for tileprocessed wireid '" << token << "'");
onTileProcessed(wireId);
return false;
};
StringVector::tokenize_foreach(lambda, wids.data(), wids.size(), ',');
docBroker->sendRequestedTiles(client_from_this());
return true;
}
else if (tokens.equals(0, "removesession"))
{
if (tokens.size() > 1 && (isDocumentOwner() || !isReadOnly()))
{
std::string sessionId = Util::encodeId(std::stoi(tokens[1]), 4);
docBroker->broadcastMessage(firstLine);
docBroker->removeSession(client_from_this());
}
else
LOG_WRN("Readonly session '" << getId() << "' trying to kill another view");
}
else if (tokens.equals(0, "renamefile"))
{
std::string encodedWopiFilename;
if (tokens.size() < 2 || !getTokenString(tokens[1], "filename", encodedWopiFilename))
{
LOG_ERR("Bad syntax for: " << firstLine);
sendTextFrameAndLogError("error: cmd=renamefile kind=syntax");
return false;
}
std::string wopiFilename;
Poco::URI::decode(encodedWopiFilename, wopiFilename);
const std::string error =
docBroker->handleRenameFileCommand(getId(), std::move(wopiFilename));
if (!error.empty())
{
sendTextFrameAndLogError(error);
return false;
}
return true;
}
else if (tokens.equals(0, "dialogevent"))
{
if (tokens.size() > 2)
{
std::string jsonString = tokens.cat("", 2);
try
{
Poco::JSON::Parser parser;
const Poco::Dynamic::Var result = parser.parse(jsonString);
const auto& object = result.extract<Poco::JSON::Object::Ptr>();
const std::string id = object->has("id") ? object->get("id").toString() : "";
if (id == "changepass" && _wopiFileInfo && !isDocumentOwner())
{
sendTextFrameAndLogError("error: cmd=dialogevent kind=cantchangepass");
return false;
}
}
catch (const std::exception& exception)
{
// Child will handle this case
}
}
return forwardToChild(firstLine, docBroker);
}
else if (tokens.equals(0, "formfieldevent") ||
tokens.equals(0, "sallogoverride") ||
tokens.equals(0, "contentcontrolevent"))
{
return forwardToChild(firstLine, docBroker);
}
else if (tokens.equals(0, "loggingleveloverride"))
{
if (tokens.size() > 0)
{
// Note that these LOG_INF() messages won't necessarily show up if the current logging
// level is higher, of course.
if (tokens.equals(1, "default"))
{
LOG_INF("Thread-local logging level being set to default ["
<< Log::getLevel()
<< "]");
Log::setThreadLocalLogLevel(Log::getLevelName());
}
else
{
try
{
auto leastVerboseAllowed = Poco::Logger::parseLevel(COOLWSD::LeastVerboseLogLevelSettableFromClient);
auto mostVerboseAllowed = Poco::Logger::parseLevel(COOLWSD::MostVerboseLogLevelSettableFromClient);
if (tokens.equals(1, "verbose"))
{
LOG_INF("Client sets thread-local logging level to the most verbose allowed ["
<< COOLWSD::MostVerboseLogLevelSettableFromClient
<< "]");
Log::setThreadLocalLogLevel(COOLWSD::MostVerboseLogLevelSettableFromClient);
LOG_INF("Thread-local logging level was set to ["
<< COOLWSD::MostVerboseLogLevelSettableFromClient
<< "]");
}
else if (tokens.equals(1, "terse"))
{
LOG_INF("Client sets thread-local logging level to the least verbose allowed ["
<< COOLWSD::LeastVerboseLogLevelSettableFromClient
<< "]");
Log::setThreadLocalLogLevel(COOLWSD::LeastVerboseLogLevelSettableFromClient);
LOG_INF("Thread-local logging level was set to ["
<< COOLWSD::LeastVerboseLogLevelSettableFromClient
<< "]");
}
else
{
auto level = Poco::Logger::parseLevel(tokens[1]);
// Note that numerically the higher priority levels are lower in value.
if (level >= leastVerboseAllowed && level <= mostVerboseAllowed)
{
LOG_INF("Thread-local logging level being set to ["
<< tokens[1]
<< "]");
Log::setThreadLocalLogLevel(tokens[1]);
}
else
{
LOG_WRN("Client tries to set logging level to ["
<< tokens[1]
<< "] which is outside of bounds ["
<< COOLWSD::LeastVerboseLogLevelSettableFromClient << ","
<< COOLWSD::MostVerboseLogLevelSettableFromClient << "]");
}
}
}
catch (const Poco::Exception &e)
{
LOG_WRN("Exception while handling loggingleveloverride message: " << e.message());
}
}
}
}
else if (tokens.equals(0, "traceeventrecording"))
{
if (COOLWSD::getConfigValue<bool>("trace_event[@enable]", false))
{
if (tokens.size() > 0)
{
if (tokens.equals(1, "start"))
{
TraceEvent::startRecording();
LOG_INF("Trace Event recording in this WSD process turned on (might have been on already)");
}
else if (tokens.equals(1, "stop"))
{
TraceEvent::stopRecording();
LOG_INF("Trace Event recording in this WSD process turned off (might have been off already)");
}
}
forwardToChild(firstLine, docBroker);
}
return true;
}
else if (tokens.equals(0, "a11ystate"))
{
if (COOLWSD::getConfigValue<bool>("accessibility.enable", false))
{
return forwardToChild(std::string(buffer, length), docBroker);
}
}
else if (tokens.equals(0, "completefunction"))
{
return forwardToChild(std::string(buffer, length), docBroker);
}
else if (tokens.equals(0, "resetaccesstoken"))
{
if (tokens.size() != 2)
{
LOG_ERR("Bad syntax for: " << tokens[0]);
sendTextFrameAndLogError("error: cmd=resetaccesstoken kind=syntax");
return false;
}
_auth.resetAccessToken(tokens[1]);
return true;
}
#if !MOBILEAPP && !WASMAPP
else if (tokens.equals(0, "switch_request"))
{
if (tokens.size() != 2)
{
LOG_ERR("Bad syntax for: " << tokens[0]);
sendTextFrameAndLogError("error: cmd=switch_request kind=syntax");
return false;
}
docBroker->switchMode(client_from_this(), tokens[1]);
return true;
}
#endif // !MOBILEAPP && !WASMAPP
else if (tokens.equals(0, "outlinestate") ||
tokens.equals(0, "downloadas") ||
tokens.equals(0, "getchildid") ||
tokens.equals(0, "gettextselection") ||
tokens.equals(0, "paste") ||
tokens.equals(0, "insertfile") ||
tokens.equals(0, "key") ||
tokens.equals(0, "textinput") ||
tokens.equals(0, "windowkey") ||
tokens.equals(0, "mouse") ||
tokens.equals(0, "windowmouse") ||
tokens.equals(0, "windowgesture") ||
tokens.equals(0, "resetselection") ||
tokens.equals(0, "saveas") ||
tokens.equals(0, "exportas") ||
tokens.equals(0, "selectgraphic") ||
tokens.equals(0, "selecttext") ||
tokens.equals(0, "windowselecttext") ||
tokens.equals(0, "setpage") ||
tokens.equals(0, "uno") ||
tokens.equals(0, "urp") ||
tokens.equals(0, "useractive") ||
tokens.equals(0, "userinactive") ||
tokens.equals(0, "paintwindow") ||
tokens.equals(0, "windowcommand") ||
tokens.equals(0, "asksignaturestatus") ||
tokens.equals(0, "rendershapeselection") ||
tokens.equals(0, "resizewindow") ||
tokens.equals(0, "removetextcontext") ||
tokens.equals(0, "rendersearchresult") ||
tokens.equals(0, "geta11yfocusedparagraph") ||
tokens.equals(0, "geta11ycaretposition"))
{
if (tokens.equals(0, "key"))
_keyEvents++;
if (isEditable() && COOLProtocol::tokenIndicatesDocumentModification(tokens))
{
docBroker->updateLastModifyingActivityTime();
}
if (!filterMessage(firstLine))
{
const std::string dummyFrame = "dummymsg";
return forwardToChild(dummyFrame, docBroker);
}
else
{
return forwardToChild(std::string(buffer, length), docBroker);
}
}
else if (tokens.equals(0, "attemptlock"))
{
return attemptLock(docBroker);
}
else if (tokens.equals(0, "blockingcommandstatus"))
{
return forwardToChild(std::string(buffer, length), docBroker);
}
else if (tokens.equals(0, "toggletiledumping"))
{
return forwardToChild(std::string(buffer, length), docBroker);
}
#if !MOBILEAPP
else if (tokens.equals(0, "routetokensanitycheck"))
{
Admin::instance().routeTokenSanityCheck();
}
#endif
else
{
LOG_ERR("Session [" << getId() << "] got unknown command [" << tokens[0] << ']');
sendTextFrameAndLogError("error: cmd=" + tokens[0] + " kind=unknown");
}
return false;
}
bool ClientSession::loadDocument(const char* /*buffer*/, int /*length*/,
const StringVector& tokens,
const std::shared_ptr<DocumentBroker>& docBroker)
{
if (tokens.size() < 2)
{
// Failed loading ends connection.
sendTextFrameAndLogError("error: cmd=load kind=syntax");
return false;
}
_viewLoadStart = std::chrono::steady_clock::now();
LOG_INF("Requesting document load from child.");
try
{
std::string timestamp, doctemplate;
int loadPart = -1;
parseDocOptions(tokens, loadPart, timestamp, doctemplate);
std::ostringstream oss;
oss << "load url=" << docBroker->getPublicUri().toString();
2018-11-13 09:04:19 +01:00
if (!getUserId().empty() && !getUserName().empty())
{
std::string encodedUserId;
2018-11-13 09:04:19 +01:00
Poco::URI::encode(getUserId(), "", encodedUserId);
oss << " authorid=" << encodedUserId;
encodedUserId.clear();
Poco::URI::encode(COOLWSD::anonymizeUsername(getUserId()), "", encodedUserId);
oss << " xauthorid=" << encodedUserId;
std::string encodedUserName;
2018-11-13 09:04:19 +01:00
Poco::URI::encode(getUserName(), "", encodedUserName);
oss << " author=" << encodedUserName;
encodedUserName.clear();
Poco::URI::encode(COOLWSD::anonymizeUsername(getUserName()), "", encodedUserName);
oss << " xauthor=" << encodedUserName;
}
2018-11-13 09:04:19 +01:00
if (!getUserExtraInfo().empty())
{
std::string encodedUserExtraInfo;
2018-11-13 09:04:19 +01:00
Poco::URI::encode(getUserExtraInfo(), "", encodedUserExtraInfo);
oss << " authorextrainfo=" << encodedUserExtraInfo; //TODO: could this include PII?
}
if (!getUserPrivateInfo().empty())
{
std::string encodedUserPrivateInfo;
Poco::URI::encode(getUserPrivateInfo(), "", encodedUserPrivateInfo);
oss << " authorprivateinfo=" << encodedUserPrivateInfo;
}
oss << " readonly=" << isReadOnly();
if (isAllowChangeComments())
{
oss << " isAllowChangeComments=true";
}
if (loadPart >= 0)
{
oss << " part=" << loadPart;
}
2018-11-13 09:04:19 +01:00
if (getHaveDocPassword())
{
2018-11-13 09:04:19 +01:00
oss << " password=" << getDocPassword();
}
2018-11-13 09:04:19 +01:00
if (!getLang().empty())
{
2018-11-13 09:04:19 +01:00
oss << " lang=" << getLang();
}
if (!getDeviceFormFactor().empty())
{
oss << " deviceFormFactor=" << getDeviceFormFactor();
}
if (!getTimezone().empty())
{
oss << " timezone=" << getTimezone();
}
if (!getSpellOnline().empty())
{
oss << " spellOnline=" << getSpellOnline();
}
if (!getTextDarkTheme().empty())
{
oss << " textDarkTheme=" << getTextDarkTheme();
}
if (!getSpreadsheetDarkTheme().empty())
{
oss << " spreadsheetDarkTheme=" << getSpreadsheetDarkTheme();
}
if (!getPresentationDarkTheme().empty())
{
oss << " presentationDarkTheme=" << getPresentationDarkTheme();
}
if (!getDrawingDarkTheme().empty())
{
oss << " drawingDarkTheme=" << getDrawingDarkTheme();
}
2018-11-13 09:04:19 +01:00
if (!getWatermarkText().empty())
{
std::string encodedWatermarkText;
2018-11-13 09:04:19 +01:00
Poco::URI::encode(getWatermarkText(), "", encodedWatermarkText);
oss << " watermarkText=" << encodedWatermarkText;
oss << " watermarkOpacity=" << COOLWSD::getConfigValue<double>("watermark.opacity", 0.2);
}
if (COOLWSD::hasProperty("security.enable_macros_execution"))
{
oss << " enableMacrosExecution=" << std::boolalpha
<< COOLWSD::getConfigValue<bool>("security.enable_macros_execution", false);
}
if (COOLWSD::hasProperty("security.macro_security_level"))
{
oss << " macroSecurityLevel=" << COOLWSD::getConfigValue<int>("security.macro_security_level", 1);
}
if (COOLWSD::getConfigValue<bool>("accessibility.enable", false))
{
oss << " accessibilityState=" << std::boolalpha << getAccessibilityState();
}
2018-11-13 09:04:19 +01:00
if (!getDocOptions().empty())
{
2018-11-13 09:04:19 +01:00
oss << " options=" << getDocOptions();
}
if (_wopiFileInfo && !_wopiFileInfo->getTemplateSource().empty())
{
oss << " template=" << _wopiFileInfo->getTemplateSource();
}
if (!getBatchMode().empty())
{
oss << " batch=" << getBatchMode();
}
#if ENABLE_FEATURE_LOCK
sendLockedInfo();
#endif
#if ENABLE_FEATURE_RESTRICTION
sendRestrictionInfo();
#endif
return forwardToChild(oss.str(), docBroker);;
}
catch (const Poco::SyntaxException&)
{
sendTextFrameAndLogError("error: cmd=load kind=uriinvalid");
}
return false;
}
#if ENABLE_FEATURE_LOCK
void ClientSession::sendLockedInfo()
{
Poco::JSON::Object::Ptr lockInfo = new Poco::JSON::Object();
CommandControl::LockManager::setTranslationPath(getLang());
lockInfo->set("IsLockedUser", CommandControl::LockManager::isLockedUser());
lockInfo->set("IsLockReadOnly", CommandControl::LockManager::isLockReadOnly());
// Poco:Dynamic:Var does not support std::unordred_set so converted to std::vector
std::vector<std::string> lockedCommandList(
CommandControl::LockManager::getLockedCommandList().begin(),
CommandControl::LockManager::getLockedCommandList().end());
lockInfo->set("LockedCommandList", lockedCommandList);
lockInfo->set("UnlockTitle", CommandControl::LockManager::getUnlockTitle());
lockInfo->set("UnlockLink", CommandControl::LockManager::getUnlockLink());
lockInfo->set("UnlockDescription", CommandControl::LockManager::getUnlockDescription());
lockInfo->set("WriterHighlights", CommandControl::LockManager::getWriterHighlights());
lockInfo->set("CalcHighlights", CommandControl::LockManager::getCalcHighlights());
lockInfo->set("ImpressHighlights", CommandControl::LockManager::getImpressHighlights());
lockInfo->set("DrawHighlights", CommandControl::LockManager::getDrawHighlights());
const Poco::URI unlockImageUri = CommandControl::LockManager::getUnlockImageUri();
if (!unlockImageUri.empty())
lockInfo->set("UnlockImageUrlPath", unlockImageUri.getPath());
CommandControl::LockManager::resetTransalatioPath();
std::ostringstream ossLockInfo;
lockInfo->stringify(ossLockInfo);
const std::string lockInfoString = ossLockInfo.str();
LOG_TRC("Sending feature locking info to client: " << lockInfoString);
sendTextFrame("featurelock: " + lockInfoString);
}
#endif
#if ENABLE_FEATURE_RESTRICTION
void ClientSession::sendRestrictionInfo()
{
Poco::JSON::Object::Ptr restrictionInfo = new Poco::JSON::Object();
restrictionInfo->set("IsRestrictedUser",
CommandControl::RestrictionManager::isRestrictedUser());
// Poco:Dynamic:Var does not support std::unordred_set so converted to std::vector
std::vector<std::string> restrictedCommandList(
CommandControl::RestrictionManager::getRestrictedCommandList().begin(),
CommandControl::RestrictionManager::getRestrictedCommandList().end());
restrictionInfo->set("RestrictedCommandList", restrictedCommandList);
std::ostringstream ossRestrictionInfo;
restrictionInfo->stringify(ossRestrictionInfo);
const std::string restrictionInfoString = ossRestrictionInfo.str();
LOG_TRC("Sending command restriction info to client: " << restrictionInfoString);
sendTextFrame("restrictedCommands: " + restrictionInfoString);
}
#endif
bool ClientSession::getCommandValues(const char *buffer, int length, const StringVector& tokens,
const std::shared_ptr<DocumentBroker>& docBroker)
{
std::string command;
if (tokens.size() != 2 || !getTokenString(tokens[1], "command", command))
return sendTextFrameAndLogError("error: cmd=commandvalues kind=syntax");
std::string cmdValues;
if (docBroker->hasTileCache() && docBroker->tileCache().getTextStream(TileCache::StreamType::CmdValues, command, cmdValues))
return sendTextFrame(cmdValues);
return forwardToChild(std::string(buffer, length), docBroker);
}
bool ClientSession::sendFontRendering(const char *buffer, int length, const StringVector& tokens,
const std::shared_ptr<DocumentBroker>& docBroker)
{
2016-11-28 00:49:17 +01:00
std::string font, text;
if (tokens.size() < 2 ||
!getTokenString(tokens[1], "font", font))
{
return sendTextFrameAndLogError("error: cmd=renderfont kind=syntax");
}
getTokenString(tokens[2], "char", text);
if (docBroker->hasTileCache())
{
Blob cachedStream = docBroker->tileCache().lookupCachedStream(TileCache::StreamType::Font, font+text);
if (cachedStream)
{
const std::string response = "renderfont: " + tokens.cat(' ', 1) + '\n';
return sendBlob(response, cachedStream);
}
}
return forwardToChild(std::string(buffer, length), docBroker);
}
bool ClientSession::sendTile(const char * /*buffer*/, int /*length*/, const StringVector& tokens,
const std::shared_ptr<DocumentBroker>& docBroker)
{
try
{
docBroker->handleTileRequest(tokens, true, client_from_this());
}
catch (const std::exception& exc)
{
LOG_ERR("Failed to process tile command: " << exc.what());
return sendTextFrameAndLogError("error: cmd=tile kind=invalid");
}
return true;
}
bool ClientSession::sendCombinedTiles(const char* /*buffer*/, int /*length*/, const StringVector& tokens,
const std::shared_ptr<DocumentBroker>& docBroker)
{
try
{
TileCombined tileCombined = TileCombined::parse(tokens);
tileCombined.setNormalizedViewId(getCanonicalViewId());
if (tileCombined.hasDuplicates())
{
LOG_ERR("Dangerous, tilecombine with duplicates is not acceptable");
return sendTextFrameAndLogError("error: cmd=tile kind=invalid");
}
docBroker->handleTileCombinedRequest(tileCombined, true, client_from_this());
}
catch (const std::exception& exc)
{
LOG_ERR("Failed to process tilecombine command: " << exc.what());
// Be forgiving and log instead of disconnecting.
// return sendTextFrameAndLogError("error: cmd=tile kind=invalid");
}
return true;
}
bool ClientSession::forwardToChild(const std::string& message,
const std::shared_ptr<DocumentBroker>& docBroker)
{
const bool binary = message.starts_with("paste") || message.starts_with("urp");
return docBroker->forwardToChild(client_from_this(), message, binary);
}
bool ClientSession::filterMessage(const std::string& message) const
{
bool allowed = true;
StringVector tokens(StringVector::tokenize(message, ' '));
// Set allowed flag to false depending on if particular WOPI properties are set
if (tokens.equals(0, "downloadas"))
{
std::string id;
wsd: fix crash when downloadas has not enough parameters ==11898==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x0000007c4f87 bp 0x7fffe45dfe90 sp 0x7fffe45df608 T0) ==11898==The signal is caused by a READ memory access. ==11898==Hint: address points to the zero page. #0 0x7c4f86 in AddressIsPoisoned lode/packages/llvm-472c6ef8b0f53061b049039f9775ab127beafbe4.src/compiler-rt/lib/asan/asan_mapping.h:397 #1 0x7c4f86 in __asan::QuickCheckForUnpoisonedRegion(unsigned long, unsigned long) lode/packages/llvm-472c6ef8b0f53061b049039f9775ab127beafbe4.src/compiler-rt/lib/asan/asan_interceptors_memintrinsics.h:31 #2 0x816436 in MemcmpInterceptorCommon(void*, int (*)(void const*, void const*, unsigned long), void const*, void const*, unsigned long) lode/packages/llvm-472c6ef8b0f53061b049039f9775ab127beafbe4.src/compiler-rt/lib/asan/../sanitizer_common/sanitizer_common_interceptors.inc:834 #3 0x816d38 in memcmp lode/packages/llvm-472c6ef8b0f53061b049039f9775ab127beafbe4.src/compiler-rt/lib/asan/../sanitizer_common/sanitizer_common_interceptors.inc:866 #4 0x7f1964437595 in std::char_traits<char>::compare(char const*, char const*, unsigned long) lode/packages/gccbuild/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/char_traits.h:310 #5 0x7f1964437595 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::compare(unsigned long, unsigned long, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const lode/packages/gccbuild/x86_64-pc-linux-gnu/libstdc++-v3/include/bits/basic_string.tcc:1391 #6 0x18e206d in LOOLProtocol::getTokenString(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&) common/Protocol.cpp:141:19 #7 0x117cc0a in ClientSession::filterMessage(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const wsd/ClientSession.cpp:940:13 #8 0x116b832 in ClientSession::_handleInput(char const*, int) wsd/ClientSession.cpp:741:14 #9 0x18f70d0 in Session::handleMessage(bool, WSOpCode, std::vector<char, std::allocator<char> >&) common/Session.cpp:230:13 Change-Id: I0c7da6c02ac62bf0bc99557517fc7c517917046c Reviewed-on: https://gerrit.libreoffice.org/c/online/+/89229 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice@gmail.com> Reviewed-by: Michael Meeks <michael.meeks@collabora.com>
2020-02-21 16:27:58 +01:00
if (tokens.size() >= 3 && getTokenString(tokens[2], "id", id))
{
if (id == "print" && _wopiFileInfo && _wopiFileInfo->getDisablePrint())
{
allowed = false;
LOG_WRN("WOPI host has disabled print for this session");
}
else if (id == "export" && _wopiFileInfo && _wopiFileInfo->getDisableExport())
{
allowed = false;
LOG_WRN("WOPI host has disabled export for this session");
}
else if (id == "slideshow" && _wopiFileInfo &&
(_wopiFileInfo->getDisableExport() || !_wopiFileInfo->getWatermarkText().empty()))
{
allowed = false;
LOG_WRN("WOPI host has disabled slideshow for this session");
}
}
else
{
allowed = false;
LOG_WRN("No value of id in downloadas message");
}
}
else if (tokens.equals(0, "gettextselection"))
{
// Copying/pasting *within* the document is fine,
// so keep .uno:Copy and .uno:Paste, but exporting is not.
if (_wopiFileInfo && _wopiFileInfo->getDisableCopy())
{
allowed = false;
LOG_WRN("WOPI host has disabled copying from the document");
}
}
return allowed;
}
void ClientSession::setReadOnly(bool bVal)
{
Session::setReadOnly(bVal);
// Also inform the client
const std::string sPerm = bVal ? "readonly" : "edit";
sendTextFrame("perm: " + sPerm);
}
void ClientSession::sendFileMode(const bool readOnly, const bool editComments)
{
std::string result = "filemode:{\"readOnly\": ";
result += readOnly ? "true": "false";
result += ", \"editComment\": ";
result += editComments ? "true": "false";
result += "}";
sendTextFrame(result);
}
void ClientSession::setLockFailed(const std::string& sReason)
{
// TODO: make this "read-only" a special one with a notification (infobar? balloon tip?)
// and a button to unlock
_isLockFailed = true;
setReadOnly(true);
sendTextFrame("lockfailed:" + sReason);
}
bool ClientSession::attemptLock(const std::shared_ptr<DocumentBroker>& docBroker)
{
if (!isReadOnly())
return true;
// We are only allowed to change into edit mode if the read-only mode is because of failed lock
if (!_isLockFailed)
return false;
std::string failReason;
const bool bResult = docBroker->attemptLock(*this, failReason);
if (bResult)
setReadOnly(false);
else
sendTextFrame("lockfailed:" + failReason);
return bResult;
}
bool ClientSession::hasQueuedMessages() const
{
return _senderQueue.size() > 0;
}
void ClientSession::writeQueuedMessages(std::size_t capacity)
{
LOG_TRC("performing writes, up to " << capacity << " bytes");
std::shared_ptr<Message> item;
std::size_t wrote = 0;
try
{
// Drain the queue, for efficient communication.
while (capacity > wrote && _senderQueue.dequeue(item) && item)
{
const std::vector<char>& data = item->data();
const auto size = data.size();
assert(size && "Zero-sized messages must never be queued for sending.");
if (item->isBinary())
{
Session::sendBinaryFrame(data.data(), size);
}
else
{
Session::sendTextFrame(data.data(), size);
}
wrote += size;
LOG_TRC("wrote " << size << ", total " << wrote << " bytes");
}
}
catch (const std::exception& ex)
{
LOG_ERR("Failed to send message " << (item ? item->abbr() : "<empty-item>")
<< " to client: " << ex.what());
}
LOG_TRC("performed write, wrote " << wrote << " bytes");
}
// NB. also see browser/src/map/Clipboard.js that does this in JS for stubs.
clipboard: fix partial content read for text/html Problem: In ClientSession::postProcessCopyPayload() the serialized form of clipboard's text/html is modified to inject meta origin and sent to the client. When this data is sent back from client to the server(possibly a different one) to set the core clipboard via "setclipboard", the parser gets confused because the 'text/html' part/flavour's data length as originally set by the core the first time does not agree with the actual length due to the meta origin injection done previously. As a result the parsed results of 'text/html' is only partial and possibly the parsing of subsequent flavours are affected. Fix: Add a preprocess step for the payload for setclipboard to remove the meta origin tag from the payload before parsing is done (in ChildSession). The original clipboard payload is created in ChildSession::getClipboard(). Note that here we encode every flavour core supports ('text/html' is just one of them) where the type and size of each flavour data is included. The problem is that later on, ClientSession::postProcessCopyPayload() injects a meta tag(which is needed in the client) into this multi favour payload without adjusting the size of the flavour (which messes up any subsequent parsing of the 'text/html' flavour section and those after it). We could adjust/correct the size in ClientSession::postProcessCopyPayload(), which is one solution but the way we do it here (string search on the whole payload!) without proper parsing seems less futuristic (things can go wrong if we change the format of packing flavour data in the payload). I feel that a less hacky solution is to remove the meta tag (current patch) to make it exactly how it was created in ChildSession::getClipboard() so that we don't have to do any error-prone size adjustments in ClientSession::postProcessCopyPayload() or when parsing the payload. Signed-off-by: Dennis Francis <dennis.francis@collabora.com> Change-Id: I285f133829ea0f2dd1cd07d17e90f794ba9a6caf
2021-10-22 09:20:19 +02:00
// See also ClientSession::preProcessSetClipboardPayload() which removes the
// <div id="meta-origin"...> tag added here.
void ClientSession::postProcessCopyPayload(const std::shared_ptr<Message>& payload)
{
// Insert our meta origin if we can
payload->rewriteDataBody([this](std::vector<char>& data) {
if (Util::findInVector(data, "clipboardcontent: content\ntext/plain") == 0)
{
// Single format and it's plain text (not HTML): no need to rewrite anything.
return false;
}
bool json = Util::findInVector(data, "textselectioncontent:\n{") == 0;
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
if (!json)
{
json = Util::findInVector(data, "clipboardcontent: content\n{") == 0;
}
std::size_t pos = Util::findInVector(data, "<body");
if (pos != std::string::npos)
{
pos = Util::findInVector(data, ">", pos);
}
// cf. TileLayer.js /_dataTransferToDocument/
if (pos != std::string::npos)
{
const std::string meta = getClipboardURI();
LOG_TRC("Inject clipboard cool origin of '" << meta << "'");
std::string origin = "<div id=\"meta-origin\" data-coolorigin=\"" + meta + "\">\n";
if (json)
{
origin = "<div id=\\\"meta-origin\\\" data-coolorigin=\\\"" + meta + "\\\">\\n";
}
data.insert(data.begin() + pos + strlen(">"), origin.begin(), origin.end());
const char* end = "</body>";
pos = Util::findInVector(data, end);
if (pos != std::string::npos)
{
origin = "</div>";
data.insert(data.begin() + pos, origin.begin(), origin.end());
}
return true;
}
else
{
LOG_DBG("Missing <body> in textselectioncontent/clipboardcontent payload: "
<< Util::dumpHex(data));
return false;
}
});
}
bool ClientSession::handleKitToClientMessage(const std::shared_ptr<Message>& payload)
{
LOG_TRC("handling kit-to-client [" << payload->abbr() << ']');
const std::string& firstLine = payload->firstLine();
const std::shared_ptr<DocumentBroker> docBroker = _docBroker.lock();
if (!docBroker)
{
LOG_ERR("No DocBroker to handle kit-to-client message: " << firstLine);
return false;
}
const bool isConvertTo = static_cast<bool>(_saveAsSocket);
if (!Util::isMobileApp())
COOLWSD::dumpOutgoingTrace(docBroker->getJailId(), getId(), firstLine);
const auto& tokens = payload->tokens();
if (tokens.equals(0, "unocommandresult:"))
{
LOG_INF("Command: " << firstLine);
const std::string stringJSON = payload->jsonString();
if (!stringJSON.empty())
{
try
{
Poco::JSON::Parser parser;
const Poco::Dynamic::Var parsedJSON = parser.parse(stringJSON);
const auto& object = parsedJSON.extract<Poco::JSON::Object::Ptr>();
if (object->get("commandName").toString() == ".uno:Save")
{
// Save to Storage and log result.
docBroker->handleSaveResponse(client_from_this(), object);
if (!isCloseFrame())
forwardToClient(payload);
return true;
}
}
catch (const std::exception& exception)
{
LOG_ERR("unocommandresult parsing failure: " << exception.what());
}
}
else
{
LOG_WRN("Expected json unocommandresult. Ignoring: " << firstLine);
}
}
else if (tokens.equals(0, "error:"))
{
std::string errorCommand;
std::string errorKind;
if (getTokenString(tokens[1], "cmd", errorCommand) &&
getTokenString(tokens[2], "kind", errorKind) )
{
if (errorCommand == "load")
{
LOG_ERR("Document load failed: " << errorKind);
if (errorKind == "passwordrequired:to-view" ||
errorKind == "passwordrequired:to-modify" ||
errorKind == "wrongpassword")
{
if (isConvertTo)
{
http::Response response(http::StatusCode::Unauthorized);
response.set("X-ERROR-KIND", errorKind);
_saveAsSocket->send(response);
// Conversion failed, cleanup fake session.
LOG_TRC("Removing save-as ClientSession after conversion error.");
// Remove us.
docBroker->removeSession(client_from_this());
// Now terminate.
docBroker->stop("Aborting saveas handler.");
}
else
{
forwardToClient(payload);
}
return false;
}
}
else
{
LOG_ERR(errorCommand << " error failure: " << errorKind);
}
}
}
else if (tokens.equals(0, "curpart:") && tokens.size() == 2)
{
//TODO: Should forward to client?
int curPart;
return getTokenInteger(tokens[1], "part", curPart);
}
else if (tokens.equals(0, "setpart:") && tokens.size() == 2)
{
if(!_isTextDocument)
{
int setPart;
if(getTokenInteger(tokens[1], "part", setPart))
{
_clientSelectedPart = setPart;
}
else if (stringToInteger(tokens[1], setPart))
{
_clientSelectedPart = setPart;
}
else
return false;
}
}
#if !MOBILEAPP
else if (tokens.size() == 3 && (tokens.equals(0, "saveas:") || tokens.equals(0, "exportas:")))
{
bool isExportAs = tokens.equals(0, "exportas:");
std::string encodedURL;
if (!getTokenString(tokens[1], "url", encodedURL))
{
LOG_ERR("Bad syntax for: " << firstLine);
// we must not return early with convert-to so that we clean up
// the session
if (!isConvertTo)
{
sendTextFrameAndLogError("error: cmd=saveas kind=syntax");
return false;
}
}
std::string encodedWopiFilename;
if (!isConvertTo && !getTokenString(tokens[2], "filename", encodedWopiFilename))
{
LOG_ERR("Bad syntax for: " << firstLine);
sendTextFrameAndLogError("error: cmd=saveas kind=syntax");
return false;
}
// Save-as completed, inform the ClientSession.
std::string wopiFilename;
Poco::URI::decode(encodedWopiFilename, wopiFilename);
// URI constructor implicitly decodes when it gets std::string as param
Poco::URI resultURL(encodedURL);
// Prepend the jail path in the normal (non-nocaps) case
if (resultURL.getScheme() == "file" && !COOLWSD::NoCapsForKit)
{
std::string relative;
if (isConvertTo || isExportAs)
Poco::URI::decode(resultURL.getPath(), relative);
else
relative = resultURL.getPath();
if (relative.size() > 0 && relative[0] == '/')
relative = relative.substr(1);
// Rewrite file:// URLs to be visible to the outside world.
const Path path(docBroker->getJailRoot(), relative);
if (Poco::File(path).exists())
{
if (!isConvertTo)
{
// Encode path for special characters (i.e '%') since Poco::URI::setPath implicitly decodes the input param
std::string encodedPath;
Poco::URI::encode(path.toString(), "", encodedPath);
resultURL.setPath(encodedPath);
}
else
{
resultURL.setPath(path.toString());
}
}
else
{
// Blank for failure.
LOG_DBG("SaveAs produced no output in '" << path.toString() << "', producing blank url.");
resultURL.clear();
}
}
LOG_TRC("Save-as URL: " << resultURL.toString());
if (!isConvertTo)
{
// Normal SaveAs - save to Storage and log result.
if (resultURL.getScheme() == "file" && !resultURL.getPath().empty())
{
// this also sends the saveas: result
LOG_TRC("Save-as path: " << resultURL.getPath());
docBroker->uploadAsToStorage(client_from_this(), resultURL.getPath(), wopiFilename,
false, isExportAs);
}
else
sendTextFrameAndLogError("error: cmd=storage kind=savefailed");
}
else
{
// using the convert-to REST API
// TODO: Send back error when there is no output.
if (!resultURL.getPath().empty())
{
LOG_TRC("Sending file: " << resultURL.getPath());
const std::string fileName = Poco::Path(resultURL.getPath()).getFileName();
http::Response response(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(response);
if (!fileName.empty())
response.set("Content-Disposition", "attachment; filename=\"" + fileName + '"');
response.setContentType("application/octet-stream");
HttpHelper::sendFileAndShutdown(_saveAsSocket, resultURL.getPath(), response);
}
// Conversion is done, cleanup this fake session.
LOG_TRC("Removing save-as ClientSession after conversion.");
// Remove us.
docBroker->removeSession(client_from_this());
// Now terminate.
docBroker->stop("Finished saveas handler.");
}
return true;
}
#endif
else if (tokens.size() == 2 && tokens.equals(0, "statechanged:"))
{
StringVector stateTokens(StringVector::tokenize(tokens[1], '='));
if (stateTokens.size() == 2 && stateTokens.equals(0, ".uno:ModifiedStatus"))
{
// Always update the modified flag in the DocBroker faithfully.
// Let it deal with the upload failure scenario and the admin console.
docBroker->setModified(stateTokens.equals(1, "true"));
}
else
{
// Set the initial settings per the user's request.
const std::pair<std::string, std::string> unoStatePair = Util::split(tokens[1], '=');
if (!docBroker->isInitialSettingSet(unoStatePair.first))
{
docBroker->setInitialSetting(unoStatePair.first);
if (unoStatePair.first == ".uno:TrackChanges")
{
if ((unoStatePair.second == "true" &&
_wopiFileInfo && _wopiFileInfo->getDisableChangeTrackingRecord() == WopiStorage::WOPIFileInfo::TriState::True) ||
(unoStatePair.second == "false" &&
_wopiFileInfo && _wopiFileInfo->getDisableChangeTrackingRecord() == WopiStorage::WOPIFileInfo::TriState::False))
{
// Toggle the TrackChanges state.
LOG_DBG("Forcing " << unoStatePair.first << " toggle per user settings.");
forwardToChild("uno .uno:TrackChanges", docBroker);
}
}
else if (unoStatePair.first == ".uno:ShowTrackedChanges")
{
if ((unoStatePair.second == "true" &&
_wopiFileInfo && _wopiFileInfo->getDisableChangeTrackingShow() == WopiStorage::WOPIFileInfo::TriState::True) ||
(unoStatePair.second == "false" &&
_wopiFileInfo && _wopiFileInfo->getDisableChangeTrackingShow() == WopiStorage::WOPIFileInfo::TriState::False))
{
// Toggle the ShowTrackChanges state.
LOG_DBG("Forcing " << unoStatePair.first << " toggle per user settings.");
forwardToChild("uno .uno:ShowTrackedChanges", docBroker);
}
}
}
}
}
else if (tokens.equals(0, "textselectioncontent:"))
{
postProcessCopyPayload(payload);
return forwardToClient(payload);
}
else if (tokens.equals(0, "clipboardcontent:"))
{
#if !MOBILEAPP // Most likely nothing of this makes sense in a mobile app
// FIXME: Ash: we need to return different content depending
// on whether this is a download-everything, or an individual
// 'download' and/or providing our helpful / user page.
// for now just for remote sockets.
LOG_TRC("Got clipboard content of size " << payload->size() << " to send to "
<< _clipSockets.size() << " sockets in state "
<< name(_state));
postProcessCopyPayload(payload);
std::size_t header;
for (header = 0; header < payload->size();)
if (payload->data()[header++] == '\n')
break;
const bool empty = header >= payload->size();
// final cleanup ...
if (!empty && (!_wopiFileInfo || !_wopiFileInfo->getDisableCopy()))
COOLWSD::SavedClipboards->insertClipboard(
_clipboardKeys, &payload->data()[header], payload->size() - header);
for (const auto& it : _clipSockets)
{
auto socket = it.lock();
if (!socket)
continue;
std::ostringstream oss;
oss << "HTTP/1.1 200 OK\r\n"
<< "Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
<< "User-Agent: " << http::getAgentString() << "\r\n"
<< "Content-Length: " << (empty ? 0 : (payload->size() - header)) << "\r\n"
<< "Content-Type: application/octet-stream\r\n"
<< "X-Content-Type-Options: nosniff\r\n"
<< "Connection: close\r\n"
<< "\r\n";
if (!empty)
{
oss.write(&payload->data()[header], payload->size() - header);
socket->setSocketBufferSize(
std::min(payload->size() + 256, std::size_t(Socket::MaximumSendBufferSize)));
}
socket->send(oss.str());
socket->shutdown();
LOG_INF("Queued " << (empty?"empty":"clipboard") << " response for send.");
}
#endif
_clipSockets.clear();
return true;
}
else if (tokens.equals(0, "disconnected:"))
{
LOG_INF("End of disconnection handshake for " << getId());
docBroker->finalRemoveSession(client_from_this());
return true;
}
else if (tokens.equals(0, "graphicselection:") || tokens.equals(0, "graphicviewselection:"))
{
if (_thumbnailSession)
{
int x, y;
if (stringToInteger(tokens[1], x) &&
stringToInteger(tokens[2], y))
{
std::ostringstream renderThumbnailCmd;
renderThumbnailCmd << "getthumbnail x=" << x << " y=" << y;
docBroker->forwardToChild(client_from_this(), renderThumbnailCmd.str());
}
}
if (payload->find("url", 3) >= 0)
{
std::string json(payload->data().data(), payload->size());
const auto it = json.find('{');
const std::string prefix = json.substr(0, it);
json.erase(0, it); // Remove the prefix to parse the purse JSON part.
Poco::JSON::Object::Ptr object;
if (JsonUtil::parseJSON(json, object))
{
const std::string url = JsonUtil::getJSONValue<std::string>(object, "url");
if (!url.empty())
{
const std::string id = JsonUtil::getJSONValue<std::string>(object, "id");
if (!id.empty())
{
docBroker->addEmbeddedMedia(
id, json); // Capture the original message with internal URL.
const std::string mediaUrl = Util::encodeURIComponent(
createPublicURI("media", id, /*encode=*/false), "&");
object->set("url", mediaUrl); // Replace the url with the public one.
object->set("mimeType", "video/mp4"); //FIXME: get this from the source json
std::ostringstream mediaStr;
object->stringify(mediaStr);
const std::string msg = prefix + mediaStr.str();
forwardToClient(std::make_shared<Message>(msg, Message::Dir::Out));
return true;
}
else
{
LOG_ERR("Invalid embeddedmedia json without id: " << json);
}
}
}
}
// Non-Media graphic selsection.
forwardToClient(payload);
return true;
}
else if (tokens.equals(0, "formfieldbutton:")) {
// Do not send redundant messages
if (_lastSentFormFielButtonMessage == firstLine)
return true;
_lastSentFormFielButtonMessage = firstLine;
}
else if (tokens.equals(0, "canonicalidchange:")) {
int viewId, canonicalId;
if (getTokenInteger(tokens[1], "viewid", viewId) &&
getTokenInteger(tokens[2], "canonicalid", canonicalId))
{
_canonicalViewId = canonicalId;
}
}
#if ENABLE_FEATURE_LOCK || ENABLE_FEATURE_RESTRICTION
else if (tokens.equals(0, "status:") && !isViewLoaded())
{
std::ostringstream blockingCommandStatus;
blockingCommandStatus << "blockingcommandstatus isRestrictedUser="
<< (CommandControl::RestrictionManager::isRestrictedUser() ? "true"
: "false")
<< " isLockedUser="
<< (CommandControl::LockManager::isLockedUser() ? "true" : "false");
docBroker->forwardToChild(client_from_this(), blockingCommandStatus.str());
}
#endif
2018-11-13 09:04:19 +01:00
if (!isDocPasswordProtected())
{
if (tokens.equals(0, "tile:"))
{
assert(false && "Tile traffic should go through the DocumentBroker-LoKit WS.");
}
else if (tokens.equals(0, "jsdialog:") && _state == ClientSession::SessionState::LOADING)
{
docBroker->setInteractive(true);
}
else if (tokens.equals(0, "status:"))
{
setState(ClientSession::SessionState::LIVE);
docBroker->setInteractive(false);
docBroker->setLoaded();
if (UnitWSD::isUnitTesting())
{
UnitWSD::get().onDocBrokerViewLoaded(docBroker->getDocKey(), client_from_this());
}
#if !MOBILEAPP
Admin::instance().setViewLoadDuration(docBroker->getDocKey(), getId(), std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - _viewLoadStart));
#endif
// position cursor for thumbnail rendering
if (_thumbnailSession)
{
//check whether we have a target!
std::ostringstream cmd;
cmd << "{";
cmd << "\"Name\":"
"{"
"\"type\":\"string\","
"\"value\":\"URL\""
"},"
"\"URL\":"
"{"
"\"type\":\"string\","
"\"value\":\"#";
cmd << getThumbnailTarget();
cmd << "\"}}";
const std::string renderThumbnailCmd = "uno .uno:OpenHyperLink " + cmd.str();
docBroker->forwardToChild(client_from_this(), renderThumbnailCmd);
}
// Wopi post load actions
if (_wopiFileInfo && !_wopiFileInfo->getTemplateSource().empty())
{
LOG_DBG("Uploading template [" << _wopiFileInfo->getTemplateSource()
<< "] to storage after loading.");
docBroker->uploadAfterLoadingTemplate(client_from_this());
}
for(auto &token : tokens)
{
// Need to get the initial part id from status message
int part = -1;
if(getTokenInteger(tokens.getParam(token), "current", part))
{
_clientSelectedPart = part;
}
int mode = 0;
if(getTokenInteger(tokens.getParam(token), "mode", mode))
_clientSelectedMode = mode;
// Get document type too
std::string docType;
if(getTokenString(tokens.getParam(token), "type", docType))
{
_isTextDocument = docType.find("text") != std::string::npos;
}
// Store our Kit ViewId
int viewId = -1;
if(getTokenInteger(tokens.getParam(token), "viewid", viewId))
_kitViewId = viewId;
}
// Forward the status response to the client.
return forwardToClient(payload);
}
else if (tokens.equals(0, "commandvalues:"))
{
const std::string stringJSON = payload->jsonString();
if (!stringJSON.empty())
{
try
{
Poco::JSON::Parser parser;
const Poco::Dynamic::Var result = parser.parse(stringJSON);
const auto& object = result.extract<Poco::JSON::Object::Ptr>();
const std::string commandName = object->has("commandName") ? object->get("commandName").toString() : "";
if (commandName == ".uno:CharFontName" ||
commandName == ".uno:StyleApply")
{
// other commands should not be cached
docBroker->tileCache().saveTextStream(TileCache::StreamType::CmdValues,
commandName, payload->data());
}
}
catch (const std::exception& exception)
{
LOG_ERR("commandvalues parsing failure: " << exception.what());
}
}
}
else if (tokens.equals(0, "invalidatetiles:"))
{
assert(firstLine.size() == payload->size() &&
"Unexpected multiline data in invalidatetiles");
// First forward invalidation
bool ret = forwardToClient(payload);
handleTileInvalidation(firstLine, docBroker);
return ret;
}
else if (tokens.equals(0, "statechanged:"))
{
if (_thumbnailSession)
{
// fallback in case we setup target at first character in the text document,
// or not existing target and we will not enter invalidatecursor second time
std::ostringstream renderThumbnailCmd;
auto position = getThumbnailPosition();
renderThumbnailCmd << "getthumbnail x=" << position.first << " y=" << position.second;
docBroker->forwardToChild(client_from_this(), renderThumbnailCmd.str());
}
}
else if (tokens.equals(0, "invalidatecursor:"))
{
assert(firstLine.size() == payload->size() &&
"Unexpected multiline data in invalidatecursor");
const std::string stringJSON = payload->jsonString();
Poco::JSON::Parser parser;
try
{
const Poco::Dynamic::Var result = parser.parse(stringJSON);
const auto& object = result.extract<Poco::JSON::Object::Ptr>();
const std::string rectangle = object->get("rectangle").toString();
StringVector rectangleTokens(StringVector::tokenize(rectangle, ','));
int x = 0, y = 0, w = 0, h = 0;
if (rectangleTokens.size() > 2 &&
stringToInteger(rectangleTokens[0], x) &&
stringToInteger(rectangleTokens[1], y))
{
if (rectangleTokens.size() > 3)
{
stringToInteger(rectangleTokens[2], w);
stringToInteger(rectangleTokens[3], h);
}
docBroker->invalidateCursor(x, y, w, h);
// session used for thumbnailing and target already was set
if (_thumbnailSession)
{
setThumbnailPosition(std::make_pair(x, y));
bool cursorAlreadyAtTargetPosition = getThumbnailTarget().empty();
if (cursorAlreadyAtTargetPosition)
{
std::ostringstream renderThumbnailCmd;
renderThumbnailCmd << "getthumbnail x=" << x << " y=" << y;
docBroker->forwardToChild(client_from_this(), renderThumbnailCmd.str());
}
else
{
// this is initial cursor position message
// wait for second invalidatecursor message
// reset target so we will proceed next time
setThumbnailTarget(std::string());
}
}
}
else
{
LOG_ERR("Unable to parse " << firstLine);
}
}
catch (const std::exception& exception)
{
LOG_ERR("invalidatecursor parsing failure: " << exception.what());
}
}
else if (tokens.equals(0, "renderfont:"))
{
std::string font, text;
if (tokens.size() < 3 ||
!getTokenString(tokens[1], "font", font))
{
LOG_ERR("Bad syntax for: " << firstLine);
return false;
}
getTokenString(tokens[2], "char", text);
assert(firstLine.size() < payload->size() && "Missing multiline data in renderfont");
docBroker->tileCache().saveStream(TileCache::StreamType::Font, font + text,
payload->data().data() + firstLine.size() + 1,
payload->data().size() - firstLine.size() - 1);
return forwardToClient(payload);
}
else if (tokens.equals(0, "extractedlinktargets:"))
{
LOG_TRC("Sending extracted link targets response.");
if (!_saveAsSocket)
LOG_ERR("Error in extractedlinktargets: not in isConvertTo mode");
else
{
const std::string stringJSON = payload->jsonString();
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.set("X-Content-Type-Options", "nosniff");
httpResponse.setBody(stringJSON, "application/json");
_saveAsSocket->sendAndShutdown(httpResponse);
}
// Now terminate.
docBroker->closeDocument("extractedlinktargets");
return true;
}
else if (tokens.equals(0, "sendthumbnail:"))
{
LOG_TRC("Sending get-thumbnail response.");
if (!_saveAsSocket)
LOG_ERR("Error in sendthumbnail: not in isConvertTo mode");
else
{
bool error = false;
if (firstLine.find("error") != std::string::npos)
error = true;
if (!error)
{
int firstLineSize = firstLine.size() + 1;
std::string thumbnail(payload->data().data() + firstLineSize, payload->data().size() - firstLineSize);
http::Response httpResponse(http::StatusCode::OK);
FileServerRequestHandler::hstsHeaders(httpResponse);
httpResponse.set("Last-Modified", Util::getHttpTimeNow());
httpResponse.set("X-Content-Type-Options", "nosniff");
httpResponse.setBody(std::move(thumbnail), "image/png");
_saveAsSocket->sendAndShutdown(httpResponse);
}
if (error)
{
http::Response httpResponse(http::StatusCode::InternalServerError);
httpResponse.set("Content-Length", "0");
_saveAsSocket->sendAndShutdown(httpResponse);
}
}
docBroker->closeDocument("thumbnailgenerated");
}
}
else
{
LOG_INF("Ignoring notification on password protected document: " << firstLine);
}
// Forward everything else.
return forwardToClient(payload);
}
bool ClientSession::forwardToClient(const std::shared_ptr<Message>& payload)
{
if (isCloseFrame())
{
LOG_TRC("peer began the closing handshake. Dropping forward message [" << payload->abbr()
<< ']');
return true;
}
enqueueSendMessage(payload);
return true;
}
void ClientSession::enqueueSendMessage(const std::shared_ptr<Message>& data)
{
if (isCloseFrame())
{
LOG_TRC("Connection closed, dropping message " << data->id());
return;
}
const std::shared_ptr<DocumentBroker> docBroker = _docBroker.lock();
LOG_CHECK_RET(docBroker && "Null DocumentBroker instance", );
docBroker->ASSERT_CORRECT_THREAD();
std::unique_ptr<TileDesc> tile;
if (data->firstTokenMatches("tile:") ||
data->firstTokenMatches("delta:"))
{
// Avoid sending tile or delta if it has the same wireID as the
// previously sent tile
tile = std::make_unique<TileDesc>(TileDesc::parse(data->firstLine()));
}
LOG_TRC("Enqueueing client message " << data->id());
std::size_t sizeBefore = _senderQueue.size();
std::size_t newSize = _senderQueue.enqueue(data);
// Track sent tile
if (tile && sizeBefore != newSize)
addTileOnFly(tile->getWireId());
}
void ClientSession::addTileOnFly(TileWireId wireId)
{
_tilesOnFly.emplace_back(wireId, std::chrono::steady_clock::now());
}
size_t ClientSession::getTilesOnFlyUpperLimit() const
{
// How many tiles we have on the visible area, set the upper limit accordingly
Util::Rectangle normalizedVisArea = getNormalizedVisibleArea();
float tilesOnFlyUpperLimit = 0;
if (normalizedVisArea.hasSurface() && getTileWidthInTwips() != 0 && getTileHeightInTwips() != 0)
{
const int tilesFitOnWidth = (normalizedVisArea.getRight() / getTileWidthInTwips()) -
(normalizedVisArea.getLeft() / getTileWidthInTwips()) + 1;
const int tilesFitOnHeight = (normalizedVisArea.getBottom() / getTileHeightInTwips()) -
(normalizedVisArea.getTop() / getTileHeightInTwips()) + 1;
const int tilesInVisArea = tilesFitOnWidth * tilesFitOnHeight;
tilesOnFlyUpperLimit = std::max(TILES_ON_FLY_MIN_UPPER_LIMIT, tilesInVisArea * 1.1f);
}
else
{
tilesOnFlyUpperLimit = 200; // Have a big number here to get all tiles requested by file opening
}
return tilesOnFlyUpperLimit;
}
void ClientSession::removeOutdatedTilesOnFly(const std::chrono::steady_clock::time_point &now)
{
size_t dropped = 0;
const auto highTimeoutMs = std::chrono::milliseconds(TILE_ROUNDTRIP_TIMEOUT_MS);
const auto lowTimeoutMs = std::chrono::milliseconds((int)(0.9 * TILE_ROUNDTRIP_TIMEOUT_MS));
// Check only the beginning of the list, tiles are ordered by timestamp
while(!_tilesOnFly.empty())
{
auto tileIter = _tilesOnFly.begin();
const auto elapsedTimeMs = std::chrono::duration_cast<
std::chrono::milliseconds>(now - tileIter->second);
if (elapsedTimeMs > highTimeoutMs ||
// once we start dropping - drop lots in a similar range of time
(dropped > 0 && elapsedTimeMs > lowTimeoutMs))
{
LOG_TRC("Tracker tileID " << tileIter->first << " was dropped because of time out ("
<< elapsedTimeMs
<< "). Tileprocessed message did not arrive in time.");
dropped++;
_tilesOnFly.erase(tileIter);
}
else
break;
}
if (dropped > 0)
LOG_WRN("client not consuming tiles; stalled for " << (TILE_ROUNDTRIP_TIMEOUT_MS/1000) << " seconds: removed tracking for " << dropped << " on the fly tiles");
}
Util::Rectangle ClientSession::getNormalizedVisibleArea() const
{
Util::Rectangle normalizedVisArea;
2018-11-14 09:07:47 +01:00
normalizedVisArea.setLeft(std::max(_clientVisibleArea.getLeft(), 0));
normalizedVisArea.setTop(std::max(_clientVisibleArea.getTop(), 0));
normalizedVisArea.setRight(_clientVisibleArea.getRight());
normalizedVisArea.setBottom(_clientVisibleArea.getBottom());
return normalizedVisArea;
}
void ClientSession::onDisconnect()
{
LOG_INF("Disconnected, current global number of connections (inclusive): "
<< COOLWSD::NumConnections);
const std::shared_ptr<DocumentBroker> docBroker = getDocumentBroker();
LOG_CHECK_RET(docBroker && "Null DocumentBroker instance", );
docBroker->ASSERT_CORRECT_THREAD();
const std::string docKey = docBroker->getDocKey();
// Keep self alive, so that our own dtor runs only at the end of this function. Without this,
// removeSession() may destroy us and then we can't call our own member functions anymore.
std::shared_ptr<ClientSession> session = client_from_this();
try
{
// Connection terminated. Destroy session.
LOG_DBG("on docKey [" << docKey << "] terminated. Cleaning up");
docBroker->removeSession(session);
}
catch (const UnauthorizedRequestException& exc)
{
LOG_ERR("Error in client request handler: " << exc.toString());
const std::string status = "error: cmd=internal kind=unauthorized";
LOG_TRC("Sending to Client [" << status << ']');
sendMessage(status);
// We are disconnecting, no need to close the socket here.
}
catch (const std::exception& exc)
{
LOG_ERR("Error in client request handler: " << exc.what());
}
try
{
if (isCloseFrame())
{
LOG_TRC("Normal close handshake.");
// Client initiated close handshake
// respond with close frame
shutdownNormal();
}
else if (!SigUtil::getShutdownRequestFlag())
{
// something wrong, with internal exceptions
LOG_TRC("Abnormal close handshake.");
closeFrame();
shutdownGoingAway();
}
else
{
LOG_TRC("Server recycling.");
closeFrame();
shutdownGoingAway();
}
}
catch (const std::exception& exc)
{
LOG_ERR("Exception while closing socket for docKey [" << docKey << "]: " << exc.what());
}
}
void ClientSession::dumpState(std::ostream& os)
{
Session::dumpState(os);
const std::shared_ptr<DocumentBroker> docBroker = _docBroker.lock();
os << "\t\tisLive: " << isLive()
<< "\n\t\tisViewLoaded: " << isViewLoaded()
<< "\n\t\tisDocumentOwner: " << isDocumentOwner()
<< "\n\t\tstate: " << name(_state)
<< "\n\t\tkeyEvents: " << _keyEvents
// << "\n\t\tvisibleArea: " << _clientVisibleArea
<< "\n\t\tclientSelectedPart: " << _clientSelectedPart
<< "\n\t\ttile size Pixel: " << _tileWidthPixel << 'x' << _tileHeightPixel
<< "\n\t\ttile size Twips: " << _tileWidthTwips << 'x' << _tileHeightTwips
<< "\n\t\tkit ViewId: " << _kitViewId
<< "\n\t\tour URL (un-trusted): " << _serverURL.getSubURLForEndpoint("")
<< "\n\t\tisTextDocument: " << _isTextDocument
<< "\n\t\tclipboardKeys[0]: " << _clipboardKeys[0]
<< "\n\t\tclipboardKeys[1]: " << _clipboardKeys[1]
<< "\n\t\tclip sockets: " << _clipSockets.size()
<< "\n\t\tproxy access:: " << _proxyAccess
<< "\n\t\tclientSelectedMode: " << _clientSelectedMode
<< "\n\t\trequestedTiles: " << getRequestedTiles().size()
<< "\n\t\tbeingRendered: " << (!docBroker ? -1 : docBroker->tileCache().countTilesBeingRenderedForSession(client_from_this(), std::chrono::steady_clock::now()));
if (_protocol)
{
uint64_t sent = 0, recv = 0;
_protocol->getIOStats(sent, recv);
os << "\n\t\tsent/keystroke: " << (double)sent/_keyEvents << " bytes";
}
os << "\n\t\tonFlyUpperLimit: " << getTilesOnFlyUpperLimit();
os << "\n\t\tonFlyCount: " << getTilesOnFlyCount();
if (_tilesOnFly.size() > 0)
os << " between wid: " << _tilesOnFly.front().first << " as of " <<
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - _tilesOnFly.front().second) << " ms "
<< " and wid: " << _tilesOnFly.back().first << " as of " <<
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - _tilesOnFly.back().second) << " ms ";
os << '\n';
_senderQueue.dumpState(os);
// FIXME: need to dump other bits ...
}
const std::string &ClientSession::getOrCreateProxyAccess()
{
if (_proxyAccess.size() <= 0)
_proxyAccess = Util::rng::getHexString(
ProxyAccessTokenLengthBytes);
return _proxyAccess;
}
void ClientSession::handleTileInvalidation(const std::string& message,
const std::shared_ptr<DocumentBroker>& docBroker)
{
docBroker->invalidateTiles(message, getCanonicalViewId());
// Skip requesting new tiles if we don't have client visible area data yet.
if(!_clientVisibleArea.hasSurface() ||
_tileWidthPixel == 0 || _tileHeightPixel == 0 ||
_tileWidthTwips == 0 || _tileHeightTwips == 0 ||
(_clientSelectedPart == -1 && !_isTextDocument))
{
return;
}
// While saving / shutting down we can get big invalidatiions: ignore them
if (isCloseFrame()) {
LOG_TRC("Session [" << getId() << "] ignoring invalidation during close: '" << message);
return;
}
int part = 0, mode = 0;
TileWireId wireId = 0;
Util::Rectangle invalidateRect = TileCache::parseInvalidateMsg(message, part, mode, wireId);
constexpr SplitPaneName panes[4] = {
TOPLEFT_PANE,
TOPRIGHT_PANE,
BOTTOMLEFT_PANE,
BOTTOMRIGHT_PANE
};
Util::Rectangle paneRects[4];
int numPanes = 0;
for(int i = 0; i < 4; ++i)
{
if(!isSplitPane(panes[i]))
continue;
Util::Rectangle rect = getNormalizedVisiblePaneArea(panes[i]);
if (rect.intersects(invalidateRect)) {
paneRects[numPanes++] = rect;
}
}
// We can ignore the invalidation if it's outside of all split-panes.
if(!numPanes)
return;
if( part == -1 ) // If no part is specified we use the part used by the client
part = _clientSelectedPart;
int normalizedViewId = getCanonicalViewId();
std::vector<TileDesc> invalidTiles;
if((part == _clientSelectedPart && mode == _clientSelectedMode) || _isTextDocument)
{
for(int paneIdx = 0; paneIdx < numPanes; ++paneIdx)
{
const Util::Rectangle& normalizedVisArea = paneRects[paneIdx];
int lastVertTile = std::ceil(normalizedVisArea.getBottom() / static_cast<double>(_tileHeightTwips));
int lastHoriTile = std::ceil(normalizedVisArea.getRight() / static_cast<double>(_tileWidthTwips));
// Iterate through visible tiles
for(int i = normalizedVisArea.getTop() / _tileHeightTwips; i <= lastVertTile; ++i)
{
for(int j = normalizedVisArea.getLeft() / _tileWidthTwips; j <= lastHoriTile; ++j)
{
// Find tiles affected by invalidation
Util::Rectangle tileRect (j * _tileWidthTwips, i * _tileHeightTwips, _tileWidthTwips, _tileHeightTwips);
if(invalidateRect.intersects(tileRect))
{
TileDesc desc(normalizedViewId, part, mode,
_tileWidthPixel, _tileHeightPixel,
j * _tileWidthTwips, i * _tileHeightTwips,
_tileWidthTwips, _tileHeightTwips, -1, 0, -1);
bool dup = false;
// Check we don't have duplicates
for (const auto &it : invalidTiles)
{
if (it == desc)
{
LOG_TRC("Duplicate tile skipped from invalidation " << desc.debugName());
dup = true;
break;
}
}
if (!dup)
{
invalidTiles.push_back(desc);
TileWireId makeDelta = 1;
// FIXME: mobile with no TileCache & flushed kit cache
// FIXME: out of (a)sync kit vs. TileCache re: keyframes ?
if (getDocumentBroker()->hasTileCache() &&
!getDocumentBroker()->tileCache().lookupTile(desc))
makeDelta = 0; // force keyframe
invalidTiles.back().setOldWireId(makeDelta);
invalidTiles.back().setWireId(0);
}
}
}
}
}
}
if(!invalidTiles.empty())
{
TileCombined tileCombined = TileCombined::create(invalidTiles);
tileCombined.setNormalizedViewId(normalizedViewId);
docBroker->handleTileCombinedRequest(tileCombined, false, client_from_this());
}
}
bool ClientSession::isSplitPane(const SplitPaneName paneName) const
{
if (paneName == BOTTOMRIGHT_PANE)
return true;
if (paneName == TOPLEFT_PANE)
return (_splitX && _splitY);
if (paneName == TOPRIGHT_PANE)
return _splitY;
if (paneName == BOTTOMLEFT_PANE)
return _splitX;
return false;
}
Util::Rectangle ClientSession::getNormalizedVisiblePaneArea(const SplitPaneName paneName) const
{
Util::Rectangle normalizedVisArea = getNormalizedVisibleArea();
if (!_splitX && !_splitY)
return paneName == BOTTOMRIGHT_PANE ? normalizedVisArea : Util::Rectangle();
int freeStartX = normalizedVisArea.getLeft() + _splitX;
int freeStartY = normalizedVisArea.getTop() + _splitY;
int freeWidth = normalizedVisArea.getWidth() - _splitX;
int freeHeight = normalizedVisArea.getHeight() - _splitY;
switch (paneName)
{
case BOTTOMRIGHT_PANE:
return Util::Rectangle(freeStartX, freeStartY, freeWidth, freeHeight);
case TOPLEFT_PANE:
return (_splitX && _splitY) ? Util::Rectangle(0, 0, _splitX, _splitY) : Util::Rectangle();
case TOPRIGHT_PANE:
return _splitY ? Util::Rectangle(freeStartX, 0, freeWidth, _splitY) : Util::Rectangle();
case BOTTOMLEFT_PANE:
return _splitX ? Util::Rectangle(0, freeStartY, _splitX, freeHeight) : Util::Rectangle();
default:
assert(false && "Unknown split-pane name");
}
return Util::Rectangle();
}
bool ClientSession::isTileInsideVisibleArea(const TileDesc& tile) const
{
if (!_splitX && !_splitY)
{
return (tile.getTilePosX() >= _clientVisibleArea.getLeft() && tile.getTilePosX() <= _clientVisibleArea.getRight() &&
tile.getTilePosY() >= _clientVisibleArea.getTop() && tile.getTilePosY() <= _clientVisibleArea.getBottom());
}
constexpr SplitPaneName panes[4] = {
TOPLEFT_PANE,
TOPRIGHT_PANE,
BOTTOMLEFT_PANE,
BOTTOMRIGHT_PANE
};
for (int i = 0; i < 4; ++i)
{
if (!isSplitPane(panes[i]))
continue;
Util::Rectangle paneRect = getNormalizedVisiblePaneArea(panes[i]);
if (tile.getTilePosX() >= paneRect.getLeft() && tile.getTilePosX() <= paneRect.getRight() &&
tile.getTilePosY() >= paneRect.getTop() && tile.getTilePosY() <= paneRect.getBottom())
return true;
}
return false;
}
// This removes the <div id="meta-origin" ...> tag which was added in
clipboard: fix partial content read for text/html Problem: In ClientSession::postProcessCopyPayload() the serialized form of clipboard's text/html is modified to inject meta origin and sent to the client. When this data is sent back from client to the server(possibly a different one) to set the core clipboard via "setclipboard", the parser gets confused because the 'text/html' part/flavour's data length as originally set by the core the first time does not agree with the actual length due to the meta origin injection done previously. As a result the parsed results of 'text/html' is only partial and possibly the parsing of subsequent flavours are affected. Fix: Add a preprocess step for the payload for setclipboard to remove the meta origin tag from the payload before parsing is done (in ChildSession). The original clipboard payload is created in ChildSession::getClipboard(). Note that here we encode every flavour core supports ('text/html' is just one of them) where the type and size of each flavour data is included. The problem is that later on, ClientSession::postProcessCopyPayload() injects a meta tag(which is needed in the client) into this multi favour payload without adjusting the size of the flavour (which messes up any subsequent parsing of the 'text/html' flavour section and those after it). We could adjust/correct the size in ClientSession::postProcessCopyPayload(), which is one solution but the way we do it here (string search on the whole payload!) without proper parsing seems less futuristic (things can go wrong if we change the format of packing flavour data in the payload). I feel that a less hacky solution is to remove the meta tag (current patch) to make it exactly how it was created in ChildSession::getClipboard() so that we don't have to do any error-prone size adjustments in ClientSession::postProcessCopyPayload() or when parsing the payload. Signed-off-by: Dennis Francis <dennis.francis@collabora.com> Change-Id: I285f133829ea0f2dd1cd07d17e90f794ba9a6caf
2021-10-22 09:20:19 +02:00
// ClientSession::postProcessCopyPayload(), else the payload parsing
// in ChildSession::setClipboard() will fail.
// To see why, refer
// 1. ChildSession::getClipboard() where the data for various
// flavours along with flavour-type and length fields are packed into the payload.
// 2. The clipboard payload parsing code in ClipboardData::read().
void ClientSession::preProcessSetClipboardPayload(std::string& payload)
{
std::size_t start = payload.find("<div id=\"meta-origin\" data-coolorigin=\"");
if (start != std::string::npos)
{
std::size_t end = payload.find("\">\n", start);
if (end == std::string::npos)
{
LOG_DBG("Found unbalanced starting meta <div> tag in setclipboard payload.");
return;
}
std::size_t len = end - start + 3;
payload.erase(start, len);
start = payload.find("</div></body>");
if (start == std::string::npos)
{
LOG_DBG("Found unbalanced ending meta <div> tag in setclipboard payload.");
return;
}
payload.erase(start, strlen("</div>"));
}
clipboard: fix partial content read for text/html Problem: In ClientSession::postProcessCopyPayload() the serialized form of clipboard's text/html is modified to inject meta origin and sent to the client. When this data is sent back from client to the server(possibly a different one) to set the core clipboard via "setclipboard", the parser gets confused because the 'text/html' part/flavour's data length as originally set by the core the first time does not agree with the actual length due to the meta origin injection done previously. As a result the parsed results of 'text/html' is only partial and possibly the parsing of subsequent flavours are affected. Fix: Add a preprocess step for the payload for setclipboard to remove the meta origin tag from the payload before parsing is done (in ChildSession). The original clipboard payload is created in ChildSession::getClipboard(). Note that here we encode every flavour core supports ('text/html' is just one of them) where the type and size of each flavour data is included. The problem is that later on, ClientSession::postProcessCopyPayload() injects a meta tag(which is needed in the client) into this multi favour payload without adjusting the size of the flavour (which messes up any subsequent parsing of the 'text/html' flavour section and those after it). We could adjust/correct the size in ClientSession::postProcessCopyPayload(), which is one solution but the way we do it here (string search on the whole payload!) without proper parsing seems less futuristic (things can go wrong if we change the format of packing flavour data in the payload). I feel that a less hacky solution is to remove the meta tag (current patch) to make it exactly how it was created in ChildSession::getClipboard() so that we don't have to do any error-prone size adjustments in ClientSession::postProcessCopyPayload() or when parsing the payload. Signed-off-by: Dennis Francis <dennis.francis@collabora.com> Change-Id: I285f133829ea0f2dd1cd07d17e90f794ba9a6caf
2021-10-22 09:20:19 +02:00
}
std::string ClientSession::processSVGContent(const std::string& svg)
{
const std::shared_ptr<DocumentBroker> docBroker = _docBroker.lock();
if (!docBroker)
{
LOG_ERR("No DocBroker to process SVG content");
return svg;
}
bool broken = false;
std::ostringstream oss;
std::string::size_type pos = 0;
for (;;)
{
static const std::string prefix = "src=\"file:///tmp/";
const auto start = svg.find(prefix, pos);
if (start == std::string::npos)
{
// Copy the rest and finish.
oss << svg.substr(pos);
break;
}
const auto startFilename = start + prefix.size();
const auto end = svg.find('"', startFilename);
if (end == std::string::npos)
{
// Broken file; leave it as-is. Better to have no video than no slideshow.
broken = true;
break;
}
auto dot = svg.find('.', startFilename);
if (dot == std::string::npos || dot > end)
dot = end;
const std::string id = svg.substr(startFilename, dot - startFilename);
oss << svg.substr(pos, start - pos);
// Store the original json with the internal, temporary, file URI.
const std::string fileUrl = svg.substr(start + 5, end - start - 5);
docBroker->addEmbeddedMedia(id, "{ \"action\":\"update\",\"id\":\"" + id + "\",\"url\":\"" +
fileUrl + "\"}");
const std::string mediaUrl =
Util::encodeURIComponent(createPublicURI("media", id, /*encode=*/false), "&");
oss << "src=\"" << mediaUrl << '"';
pos = end + 1;
}
return broken ? svg : oss.str();
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */