forked from CollaboraOnline/online
-
Notifications
You must be signed in to change notification settings - Fork 0
/
DocumentBroker.hpp
1792 lines (1478 loc) · 69 KB
/
DocumentBroker.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* -*- 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/.
*/
#pragma once
#include <atomic>
#include <chrono>
#include <cstddef>
#include <map>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <utility>
#include <Poco/SharedPtr.h>
#include <Poco/URI.h>
#include "Log.hpp"
#include "QuarantineUtil.hpp"
#include "TileDesc.hpp"
#include "Util.hpp"
#include "net/Socket.hpp"
#include "net/WebSocketHandler.hpp"
#include "Storage.hpp"
#include "common/SigUtil.hpp"
#include "common/Session.hpp"
#if !MOBILEAPP
#include "Admin.hpp"
#include <wopi/WopiStorage.hpp>
#else // MOBILEAPP
#include <MobileApp.hpp>
#endif // MOBILEAPP
// Forwards.
class PrisonerRequestDispatcher;
class DocumentBroker;
struct LockContext;
class TileCache;
class Message;
namespace Poco {
namespace JSON {
class Object;
};
};
class UrpHandler : public SimpleSocketHandler
{
public:
UrpHandler(ChildProcess* process) : _childProcess(process)
{
}
void onConnect(const std::shared_ptr<StreamSocket>& socket) override
{
_socket = socket;
setLogContext(socket->getFD());
}
void handleIncomingMessage(SocketDisposition& /*disposition*/) override;
int getPollEvents(std::chrono::steady_clock::time_point /* now */,
int64_t & /* timeoutMaxMicroS */) override
{
return POLLIN;
}
void performWrites(std::size_t /*capacity*/) override {}
private:
// The socket that owns us (we can't own it).
std::weak_ptr<StreamSocket> _socket;
ChildProcess* _childProcess;
};
/// A ChildProcess object represents a Kit process that hosts a document and manipulates the
/// document using the LibreOfficeKit API. It isn't actually a child of the WSD process, but a
/// grandchild. The comments loosely talk about "child" anyway.
class ChildProcess final : public WSProcess
{
public:
/// @param pid is the process ID of the child.
/// @param socket is the underlying Socket to the child.
template <typename T>
ChildProcess(const pid_t pid, const std::string& jailId,
const std::shared_ptr<StreamSocket>& socket, const T& request)
: WSProcess("ChildProcess", pid, socket,
std::make_shared<WebSocketHandler>(socket, request))
, _jailId(jailId)
, _smapsFD(-1)
{
int urpFromKitFD = socket->getIncomingFD(URPFromKit);
int urpToKitFD = socket->getIncomingFD(URPToKit);
if (urpFromKitFD != -1 && urpToKitFD != -1)
{
_urpFromKit = StreamSocket::create<StreamSocket>(
std::string(), urpFromKitFD, Socket::Type::Unix,
false, std::make_shared<UrpHandler>(this));
_urpToKit = StreamSocket::create<StreamSocket>(
std::string(), urpToKitFD, Socket::Type::Unix,
false, std::make_shared<UrpHandler>(this));
}
}
ChildProcess(ChildProcess&& other) = delete;
bool sendUrpMessage(const std::string& message)
{
if (!_urpToKit)
return false;
if (message.size() < 4)
{
LOG_ERR("URP Message too short");
return false;
}
_urpToKit->send(message.data() + 4, message.size() - 4);
return true;
}
virtual ~ChildProcess()
{
if (_urpFromKit)
_urpFromKit->shutdown();
if (_urpToKit)
_urpToKit->shutdown();
::close(_smapsFD);
}
const ChildProcess& operator=(ChildProcess&& other) = delete;
void setDocumentBroker(const std::shared_ptr<DocumentBroker>& docBroker);
std::shared_ptr<DocumentBroker> getDocumentBroker() const { return _docBroker.lock(); }
const std::string& getJailId() const { return _jailId; }
void setSMapsFD(int smapsFD) { _smapsFD = smapsFD;}
int getSMapsFD(){ return _smapsFD; }
void moveSocketFromTo(const std::shared_ptr<SocketPoll> &from, SocketPoll &to)
{
to.takeSocket(from, getSocket());
}
private:
const std::string _jailId;
std::weak_ptr<DocumentBroker> _docBroker;
std::shared_ptr<StreamSocket> _urpFromKit;
std::shared_ptr<StreamSocket> _urpToKit;
int _smapsFD;
};
class RequestDetails;
class ClientSession;
/// DocumentBroker is responsible for setting up a document in jail and brokering loading it from
/// Storage and saving it back.
/// Contains URI, physical path, etc.
/// There is one DocumentBroker object in the WSD process for each document that is open (in 1..n sessions).
/// The Document State:
///
/// The Document lifecycle is managed through
/// the DocumentState class, which encapsulates
/// the different stages of the Document's
/// main-sequence events:
///
/// To disambiguate between Storage and Core, we
/// use 'Download' for Reading from the Storage,
/// and 'Load' for Loading a document in Core.
/// Similarly, we 'Upload' to Storage after we
/// 'Save' the document in Core.
///
/// None: the Document doesn't exist, pending downloading.
/// Downloading: the Document is being downloaded from Storage.
/// Loading: the Document is being loaded into Core.
/// Live: Steady-state; the document is available (see below).
/// Destroying: End-of-life, marked to save/upload and destroy.
/// Destroyed: Unloading complete, destruction of class pending.
///
/// The Document Data State:
///
/// There are three locations to track:
/// 1) the Storage (wopi host)
/// 2) the Local file on disk (in jail)
/// 3) in memory (in Core).
///
/// We download the document from Storage to disk, then
/// we load it in memory (Core). From then on, we track the
/// state after modification (in memory), saving (to disk),
/// and uploading (to Storage).
///
/// Download: Storage -> Local
/// Load: Local -> Core
/// Save: Core -> Local
/// Upload: Local -> Storage
///
/// This is the state matrix during the key operations:
/// |-------------------------------------------|
/// | State | Storage | Local | Core |
/// |-------------|---------|---------|---------|
/// | Downloading | Reading | Writing | Idle |
/// | Loading | Idle | Reading | Writing |
/// | Saving | Idle | Writing | Reading |
/// | Uploading | Writing | Reading | Idle |
/// |-------------------------------------------|
///
/// Downloading is done synchronously, for now, but
/// is provisioned for async in the state machine.
/// Similarly, we could download asynchronously,
/// but there is little to gain by doing that,
/// since nothing much can happen without, or
/// before, loading a document.
///
/// The decision for Saving and Uploading are separate.
/// Without the user's intervention, we auto-save
/// when the user has been idle for some configurable
/// time, or when a certain configurable minimum time
/// has elapsed since the last save (regardless of user
/// activity). Once we get the save result from Core
/// (and ideally with success), we upload the document
/// immediately. Previously, this was a synchronous
/// process, which is now being reworked into an async.
///
/// The user can invoke both Save and Upload operations
/// however, and in more than one way.
/// Saving can of course be done by simply invoking the
/// command, which also uploads.
/// Forced Uploading has a narrower use-case: when the
/// Storage has a newer version of the document,
/// uploading fails with 'document conflict' error, which
/// the user can override by forcing uploading to Storage,
/// thereby overwriting the Storage version with the
/// current one.
/// Then there are the Save-As and Rename commands, which
/// only affect the document in Storage by invoking
/// the upload functionality with special headers.
///
/// When either of these operations fails, the next
/// opportunity to review potential actions is during
/// the next poll cycle.
/// To separate these two operations in code and in time,
/// we need to track the document version in each of
/// Core and Storage. That is, when the document is saved
/// a newer 'version number' is assigned, so that it would
/// be different from the 'version number' of the document
/// in Storage. The easiest way to achieve this is by
/// using the modified time on the file on disk. While
/// this has certain limitations, in practice it's a
/// good solution. We expect each time Core saves the
/// Document to disk, the file's timestamp will change.
/// Each time we Upload a version of the Document to
/// Storage, we track the local file's timestamp that we
/// uploaded. We then need to Upload only when the last
/// Uploaded timestamp is different from that on disk.
/// Although it's technically possible for the system
/// clock to change, it's unlikely for the timestamp to
/// be identical to the last Uploaded one, down to the
/// millisecond.
///
/// This way, if, say, Uploading fails after
/// Saving, if the subsequent Save fails, we don't skip
/// Uploading, since the Storage would still be outdated.
/// Similarly, if after Saving we fail to Upload, a
/// subsequent Save might yield 'unmodified' result and
/// fail to Save a new copy of the document. This should
/// not skip Uploading, since the document on disk is
/// still newer than the one in Storage.
///
/// Notice that we cannot compare the timestamp of the
/// file on disk to the timestamp returned from Storage.
/// For one, the Storage might not even provide a
/// timestamp (or a valid one). But more importantly,
/// the timestamp on disk might not be comparable to
/// that in Storage (due to timezone and/or precision
/// differences).
///
/// Two new managers are provisioned to mind about these
/// two domains: SaveManager and StorageManager.
/// SaveManager is reponsible for tracking the operations
/// between Core and local disk, while StorageManager
/// for those between Storage and local disk.
/// In practice, each represents and tracks the state of
/// the Document in Core and Storage, respectively.
///
class DocumentBroker : public std::enable_shared_from_this<DocumentBroker>
{
class DocumentBrokerPoll;
void setupPriorities();
public:
/// How to prioritize this document.
enum class ChildType {
Interactive, Batch
};
DocumentBroker(ChildType type, const std::string& uri, const Poco::URI& uriPublic,
const std::string& docKey, unsigned mobileAppDocId,
std::unique_ptr<WopiStorage::WOPIFileInfo> wopiFileInfo);
protected:
/// Used by derived classes.
DocumentBroker(ChildType type, const std::string& uri, const Poco::URI& uriPublic,
const std::string& docKey)
: DocumentBroker(type, uri, uriPublic, docKey, /*mobileAppDocId=*/0,
/*wopiFileInfo=*/nullptr)
{
}
public:
virtual ~DocumentBroker();
/// Called when removed from the DocBrokers list
virtual void dispose() {}
/// setup the transfer of a socket into this DocumentBroker poll.
void setupTransfer(SocketDisposition &disposition,
SocketDisposition::MoveFunction transferFn);
/// setup the transfer of a socket into this DocumentBroker poll.
void setupTransfer(const std::shared_ptr<StreamSocket>& socket,
const SocketDisposition::MoveFunction& transferFn);
/// Flag for termination. Note that this doesn't save any unsaved changes in the document
void stop(const std::string& reason);
/// Hard removes a session, only for ClientSession.
void finalRemoveSession(const std::shared_ptr<ClientSession>& session);
/// Create new client session
std::shared_ptr<ClientSession> createNewClientSession(
const std::shared_ptr<ProtocolHandlerInterface> &ws,
const std::string& id,
const Poco::URI& uriPublic,
const bool isReadOnly,
const RequestDetails &requestDetails);
/// Find or create a new client session for the PHP proxy
void handleProxyRequest(
const std::string& id,
const Poco::URI& uriPublic,
const bool isReadOnly,
const RequestDetails &requestDetails,
const std::shared_ptr<StreamSocket> &socket);
/// Thread safe termination of this broker if it has a lingering thread
void joinThread();
/// Notify that the load has completed
virtual void setLoaded();
/// Notify that the document has dialogs before load
virtual void setInteractive(bool value);
/// If not yet locked, try to lock
bool attemptLock(ClientSession& session, std::string& failReason);
bool isDocumentChangedInStorage() { return _documentChangedInStorage; }
/// Invoked by the client to rename the document filename.
/// Returns an error message in case of failure, otherwise an empty string.
std::string handleRenameFileCommand(std::string sessionId, std::string newFilename);
/// Handle the save response from Core and upload to storage as necessary.
/// Also notifies clients of the result.
void handleSaveResponse(const std::shared_ptr<ClientSession>& session,
const Poco::SharedPtr<Poco::JSON::Object>& json);
/// Check if uploading is needed, and start uploading.
/// The current state of uploading must be introspected separately.
void checkAndUploadToStorage(const std::shared_ptr<ClientSession>& session, bool justSaved);
/// Upload the document to Storage if it needs persisting.
/// Results are logged and broadcast to users.
void uploadToStorage(const std::shared_ptr<ClientSession>& session, bool force);
/// UploadAs the document to Storage, with a new name.
/// @param uploadAsPath Absolute path to the jailed file.
void uploadAsToStorage(const std::shared_ptr<ClientSession>& session,
const std::string& uploadAsPath, const std::string& uploadAsFilename,
const bool isRename, const bool isExport);
/// Uploads the document right after loading from a template.
/// Template-loading requires special handling because the
/// document changes once loaded into a non-template format.
void uploadAfterLoadingTemplate(const std::shared_ptr<ClientSession>& session);
bool isModified() const { return _isModified; }
void setModified(const bool value);
/// Save the document if the document is modified.
/// @param force when true, will force saving if there
/// has been any recent activity after the last save.
/// @param dontSaveIfUnmodified when true, save will fail if the document is not modified.
/// @return true if attempts to save or it also waits
/// and receives save notification. Otherwise, false.
bool autoSave(const bool force, const bool dontSaveIfUnmodified);
/// Saves the document and stops if there was nothing to autosave.
void autoSaveAndStop(const std::string& reason);
bool isAsyncUploading() const;
Poco::URI getPublicUri() const { return _uriPublic; }
const std::string& getJailId() const { return _jailId; }
const std::string& getDocKey() const { return _docKey; }
const std::string& getFilename() const { return _filename; };
TileCache& tileCache() { return *_tileCache; }
bool hasTileCache() { return _tileCache != nullptr; }
bool isAlive() const;
/// Are we running in either shutdown, or the polling thread.
/// Asserts in the debug builds, otherwise just logs.
void assertCorrectThread(const char* filename = "?", int line = 0) const;
/// Pretty print internal state to a stream.
void dumpState(std::ostream& os);
std::string getJailRoot() const;
/// Add a new session. Returns the new number of sessions.
std::size_t addSession(const std::shared_ptr<ClientSession>& session,
std::unique_ptr<WopiStorage::WOPIFileInfo> wopiFileInfo = nullptr);
/// Removes a session by ID. Returns the new number of sessions.
std::size_t removeSession(const std::shared_ptr<ClientSession>& session);
/// Add a callback to be invoked in our polling thread.
void addCallback(const SocketPoll::CallbackFn& fn);
/// Transfer this socket into our polling thread / loop.
void addSocketToPoll(const std::shared_ptr<StreamSocket>& socket);
void alertAllUsers(const std::string& msg);
void alertAllUsers(const std::string& cmd, const std::string& kind)
{
alertAllUsers("error: cmd=" + cmd + " kind=" + kind);
}
/// Sets the log level of kit.
void setKitLogLevel(const std::string& level);
/// Invalidate the cursor position.
void invalidateCursor(int x, int y, int w, int h)
{
_cursorPosX = x;
_cursorPosY = y;
_cursorWidth = w;
_cursorHeight = h;
}
void invalidateTiles(const std::string& tiles, int normalizedViewId)
{
// Remove from cache.
_tileCache->invalidateTiles(tiles, normalizedViewId);
}
void handleTileRequest(const StringVector &tokens, bool forceKeyframe,
const std::shared_ptr<ClientSession>& session);
void handleTileCombinedRequest(TileCombined& tileCombined, bool forceKeyframe,
const std::shared_ptr<ClientSession>& session);
void sendRequestedTiles(const std::shared_ptr<ClientSession>& session);
void sendTileCombine(const TileCombined& tileCombined);
enum ClipboardRequest {
CLIP_REQUEST_SET,
CLIP_REQUEST_GET,
CLIP_REQUEST_GET_RICH_HTML_ONLY,
CLIP_REQUEST_GET_HTML_PLAIN_ONLY,
};
void handleClipboardRequest(ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket,
const std::string &viewId, const std::string &tag,
const std::shared_ptr<std::string> &data);
static bool lookupSendClipboardTag(const std::shared_ptr<StreamSocket> &socket,
const std::string &tag, bool sendError = false);
void handleMediaRequest(std::string range, const std::shared_ptr<Socket>& socket, const std::string& tag);
/// True if any flag to unload or terminate is set.
bool isUnloading() const
{
return _docState.isMarkedToDestroy() || _stop || _docState.isUnloadRequested() ||
_docState.isCloseRequested() || SigUtil::getShutdownRequestFlag();
}
/// True if any flag to unload or terminate is set.
bool isUnloadingUnrecoverably() const
{
return _docState.isMarkedToDestroy() || _stop || _docState.isCloseRequested() ||
SigUtil::getShutdownRequestFlag();
}
bool isMarkedToDestroy() const { return _docState.isMarkedToDestroy() || _stop; }
virtual bool handleInput(const std::shared_ptr<Message>& message);
/// Forward a message from client session to its respective child session.
bool forwardToChild(const std::shared_ptr<ClientSession>& session, const std::string& message,
bool binary = false);
int getRenderedTileCount() { return _debugRenderedTileCount; }
/// Ask the document broker to close. Makes sure that the document is saved.
void closeDocument(const std::string& reason);
/// Flag that we have been disconnected from the Kit and request unloading.
void disconnectedFromKit(bool unexpected);
/// Get the PID of the associated child process
pid_t getPid() const { return _childProcess ? _childProcess->getPid() : 0; }
/// Update the last activity time to now.
/// Best to be inlined as it's called frequently.
void updateLastActivityTime()
{
_lastActivityTime = std::chrono::steady_clock::now();
// posted to admin console in the main polling loop.
}
/// Sets the last activity timestamp that is most likely to modify the document.
void updateLastModifyingActivityTime()
{
_lastModifyActivityTime = std::chrono::steady_clock::now();
}
/// This updates the editing sessionId which is used for auto-saving.
void updateEditingSessionId(const std::string& viewId)
{
if (_lastEditingSessionId != viewId)
_lastEditingSessionId = viewId;
}
/// User wants to issue a save on the document.
bool manualSave(const std::shared_ptr<ClientSession>& session, bool dontTerminateEdit,
bool dontSaveIfUnmodified, const std::string& extendedData);
/// Sends a message to all sessions.
/// Returns the number of sessions sent the message to.
std::size_t broadcastMessage(const std::string& message) const;
/// Sends a message to all sessions except for the session passed as the param
void broadcastMessageToOthers(const std::string& message, const std::shared_ptr<ClientSession>& _session) const;
/// Broadcasts 'blockui' command to all users with an optional message.
void blockUI(const std::string& msg)
{
broadcastMessage("blockui: " + msg);
}
/// Broadcasts 'unblockui' command to all users.
void unblockUI()
{
broadcastMessage("unblockui: ");
}
/// Returns true iff an initial setting by the given name is already initialized.
bool isInitialSettingSet(const std::string& name) const;
/// Sets the initialization flag of a given initial setting.
void setInitialSetting(const std::string& name);
/// For testing only [!]
std::vector<std::shared_ptr<ClientSession>> getSessionsTestOnlyUnsafe();
/// Estimate memory usage / bytes
std::size_t getMemorySize() const;
/// Get URL for corresponding download id if registered, or empty string otherwise
std::string getDownloadURL(const std::string& downloadId);
/// Remove download id mapping
void unregisterDownloadId(const std::string& downloadId);
/// Add embedded media objects. Returns json with external URL.
void addEmbeddedMedia(const std::string& id, const std::string& json);
/// Remove embedded media objects.
void removeEmbeddedMedia(const std::string& json);
void onUrpMessage(const char* data, size_t len);
void setMigrationMsgReceived() { _migrateMsgReceived = true; }
#if !MOBILEAPP && !WASMAPP
/// Switch between Online and Offline modes.
void switchMode(const std::shared_ptr<ClientSession>& session, const std::string& mode);
#endif // !MOBILEAPP && !WASMAPP
private:
/// Get the session that can write the document for save / locking / uploading.
/// Note that if there is no loaded and writable session, the first will be returned.
std::shared_ptr<ClientSession> getWriteableSession() const;
void refreshLock();
/// Downloads the document ahead-of-time.
bool downloadAdvance(const std::string& jailId, const Poco::URI& uriPublic,
std::unique_ptr<WopiStorage::WOPIFileInfo> wopiFileInfo);
/// Loads a document from the public URI into the jail.
bool download(const std::shared_ptr<ClientSession>& session, const std::string& jailId,
const Poco::URI& uriPublic,
std::unique_ptr<WopiStorage::WOPIFileInfo> wopiFileInfo);
/// Actual document download and post-download processing.
/// Must be called only when creating the storage for the first time.
bool doDownloadDocument(const std::shared_ptr<ClientSession>& session,
const Authorization& auth, const std::string& templateSource,
const std::string& filename,
std::chrono::milliseconds& getFileCallDurationMs);
#if !MOBILEAPP
/// Updates the Session with the wopiFileInfo given.
/// Returns the templateSource, if any.
std::string updateSessionWithWopiInfo(const std::shared_ptr<ClientSession>& session,
WopiStorage* wopiStorage,
std::unique_ptr<WopiStorage::WOPIFileInfo> wopiFileInfo);
/// Process the configured plugins, if any, after downloading the document file.
bool processPlugins(std::string& localPath);
#endif //!MOBILEAPP
bool isLoaded() const { return _docState.hadLoaded(); }
bool isInteractive() const { return _docState.isInteractive(); }
/// Updates the document's lock in storage to either locked or unlocked.
/// Returns true iff the operation was successful.
bool updateStorageLockState(ClientSession& session, bool lock, std::string& error);
std::size_t getIdleTimeSecs() const
{
const auto duration = (std::chrono::steady_clock::now() - _lastActivityTime);
return std::chrono::duration_cast<std::chrono::seconds>(duration).count();
}
void handleTileResponse(const std::shared_ptr<Message>& message);
void handleDialogPaintResponse(const std::vector<char>& payload, bool child);
void handleTileCombinedResponse(const std::shared_ptr<Message>& message);
void handleDialogRequest(const std::string& dialogCmd);
/// Invoked to issue a save before renaming the document filename.
void startRenameFileCommand();
/// Finish handling the renamefile command.
void endRenameFileCommand();
/// Shutdown all client connections with the given reason.
void shutdownClients(const std::string& closeReason);
/// This gracefully terminates the connection
/// with the child and cleans up ChildProcess etc.
void terminateChild(const std::string& closeReason);
#if !MOBILEAPP && !WASMAPP
/// Invoked to switch from Online to Offline mode.
void startSwitchingToOffline(const std::shared_ptr<ClientSession>& session);
/// Finish switching to Offline.
void endSwitchingToOffline();
/// Invoked to switch from Offline to Online mode.
void startSwitchingToOnline();
/// Do the switching when all is ready.
void switchToOffline();
/// Finish switching to Online.
void endSwitchingToOnline();
#endif // !MOBILEAPP && !WASMAPP
/// Encodes whether or not saving is possible
/// (regardless of whether we need to or not).
STATE_ENUM(
CanSave,
Yes, //< Saving is possible.
NoKit, //< There is no Kit.
NotLoaded, //< No document is loaded.
NoWriteSession, //< No available session can write.
);
/// Returns the state of whether saving is possible.
/// (regardless of whether we need to or not).
CanSave canSaveToDisk() const
{
if (_docState.isDisconnected() || getPid() <= 0)
{
return CanSave::NoKit;
}
if (!isLoaded())
{
return CanSave::NotLoaded;
}
if (_sessions.empty() || !getWriteableSession())
{
return CanSave::NoWriteSession;
}
return CanSave::Yes;
}
/// Encodes whether or not uploading is possible.
/// (regardless of whether we need to or not).
STATE_ENUM(
CanUpload,
Yes, //< Uploading is possible.
NoStorage, //< Storage instance missing.
);
/// Returns the state of whether uploading is possible.
/// (regardless of whether we need to or not).
CanUpload canUploadToStorage() const
{
return _storage ? CanUpload::Yes : CanUpload::NoStorage;
}
/// Encodes whether or not uploading is needed.
STATE_ENUM(NeedToUpload,
No, //< No need to upload, data up-to-date.
Yes, //< Data is out of date.
);
/// Returns the state of the need to upload.
/// This includes out-of-date Document in Storage or
/// always_save_on_exit.
NeedToUpload needToUploadToStorage() const;
/// Returns true iff the document on disk is newer than the one in Storage.
bool isStorageOutdated() const;
/// Upload the doc to the storage.
void uploadToStorageInternal(const std::shared_ptr<ClientSession>& session,
const std::string& saveAsPath, const std::string& saveAsFilename,
const bool isRename, const bool isExport, const bool force);
/// Handles the completion of uploading to storage, both success and failure cases.
void handleUploadToStorageResponse(const StorageBase::UploadResult& uploadResult);
/// Sends the .uno:Save command to LoKit.
bool sendUnoSave(const std::shared_ptr<ClientSession>& session, bool dontTerminateEdit = true,
bool dontSaveIfUnmodified = true, bool isAutosave = false,
const std::string& extendedData = std::string());
/**
* Report back the save result to PostMessage users (Action_Save_Resp)
* @param success: Whether saving was successful
* @param result: Short message why saving was (not) successful
* @param errorMsg: Long error msg (Error message from WOPI host if any)
*/
void broadcastSaveResult(bool success, const std::string& result = std::string(),
const std::string& errorMsg = std::string());
/// Broadcasts to all sessions the last modification time of the document.
void broadcastLastModificationTime(const std::shared_ptr<ClientSession>& session = nullptr) const;
/// True if there has been activity from a client after we last *requested* saving,
/// since there are race conditions vis-a-vis user activity while saving.
bool haveActivityAfterSaveRequest() const
{
return _saveManager.lastSaveRequestTime() < _lastActivityTime;
}
/// True if there has been potentially *document-modifying* activity from a client
/// after we last *requested* saving, since there are race conditions while saving.
bool haveModifyActivityAfterSaveRequest() const
{
return _saveManager.lastSaveRequestTime() < _lastModifyActivityTime;
}
/// Encodes whether or not saving is needed.
STATE_ENUM(NeedToSave,
No, //< No need to save, data up-to-date.
Maybe, //< We have activity post saving.
Yes_Modified, //< Data is out of date.
Yes_LastSaveFailed, //< Yes, need to produce file on disk.
);
/// Returns the state of the need to save.
NeedToSave needToSaveToDisk() const;
/// True if we know the doc is modified or
/// if there has been activity from a client after we last *requested* saving,
/// since there are race conditions vis-a-vis user activity while saving.
bool isPossiblyModified() const
{
if (haveModifyActivityAfterSaveRequest())
{
// Always assume possible modification when we have
// user input after sending a .uno:Save, due to racing.
return true;
}
if (_isViewFileExtension)
{
// ViewFileExtensions do not update the ModifiedStatus,
// but, we want a success save anyway (including unmodified).
return !_saveManager.lastSaveSuccessful();
}
// Regulard editable files, rely on the ModifiedStatus.
return isModified();
}
/// True iff there is at least one non-readonly session other than the given.
/// Since only editable sessions can save, we need to use the last to
/// save modified documents, otherwise we'll potentially have to save on
/// every editable session disconnect, lest we lose data due to racing.
bool haveAnotherEditableSession(const std::string& id) const;
/// Returns the number of active sessions.
/// This includes only those that are loaded and not waiting disconnection.
std::size_t countActiveSessions() const;
/// Loads a new session and adds to the sessions container.
std::size_t addSessionInternal(const std::shared_ptr<ClientSession>& session,
std::unique_ptr<WopiStorage::WOPIFileInfo> wopiFileInfo);
/// Starts the Kit <-> DocumentBroker shutdown handshake
void disconnectSessionInternal(const std::shared_ptr<ClientSession>& session);
/// Forward a message from child session to its respective client session.
bool forwardToClient(const std::shared_ptr<Message>& payload);
/// The thread function that all of the I/O for all sessions
/// associated with this document.
void pollThread();
/// Sum the I/O stats from all connected sessions
void getIOStats(uint64_t &sent, uint64_t &recv);
/// Returns true iff this is a Convert-To request.
/// This is needed primarily for security reasons,
/// because we can't trust the given file-path is
/// a convert-to request or doctored to look like one.
virtual bool isConvertTo() const { return false; }
/// Request manager.
/// Encapsulates common fields for
/// Save and Upload requests.
class RequestManager final
{
public:
RequestManager(std::chrono::milliseconds minTimeBetweenRequests)
: _minTimeBetweenRequests(minTimeBetweenRequests)
, _lastRequestTime(now())
, _lastResponseTime(now())
, _lastRequestDuration(0)
, _lastRequestFailureCount(0)
{
}
/// Sets the time the last request was made to now.
void markLastRequestTime() { _lastRequestTime = now(); }
/// Returns the time the last request was made.
std::chrono::steady_clock::time_point lastRequestTime() const { return _lastRequestTime; }
/// How much time passed since the last request,
/// regardless of whether we got a response or not.
const std::chrono::milliseconds timeSinceLastRequest(
const std::chrono::steady_clock::time_point now = RequestManager::now()) const
{
return std::chrono::duration_cast<std::chrono::milliseconds>(now - _lastRequestTime);
}
/// True iff there is an active request and it has timed out.
bool hasLastRequestTimedOut(std::chrono::milliseconds timeoutMs) const
{
return isActive() && timeSinceLastRequest() >= timeoutMs;
}
/// Sets the time the last response was received to now.
void markLastResponseTime()
{
_lastResponseTime = now();
_lastRequestDuration = std::chrono::duration_cast<std::chrono::milliseconds>(
_lastResponseTime - _lastRequestTime);
}
/// Returns the time the last response was received.
std::chrono::steady_clock::time_point lastResponseTime() const { return _lastResponseTime; }
/// Returns the duration of the last request.
std::chrono::milliseconds lastRequestDuration() const { return _lastRequestDuration; }
/// How much time passed since the last response,
/// regardless of whether there is a newer request or not.
const std::chrono::milliseconds timeSinceLastResponse(
const std::chrono::steady_clock::time_point now = RequestManager::now()) const
{
return std::chrono::duration_cast<std::chrono::milliseconds>(now - _lastResponseTime);
}
/// Returns true iff there is an active request in progress.
bool isActive() const { return _lastResponseTime < _lastRequestTime; }
/// The minimum time to wait between requests.
std::chrono::milliseconds minTimeBetweenRequests() const { return _minTimeBetweenRequests; }
/// Checks whether or not we can issue a new request now.
/// Returns true iff there is no active request and sufficient
/// time has elapsed since the last request, including that
/// more time than half the last request's duration has passed.
/// When unloading, we reduce throttling significantly.
bool canRequestNow(bool unloading) const
{
const std::chrono::milliseconds minTimeBetweenRequests =
unloading ? _minTimeBetweenRequests / 10 : _minTimeBetweenRequests;
const auto now = RequestManager::now();
return !isActive() && std::min(timeSinceLastRequest(now), timeSinceLastResponse(now)) >=
std::max(minTimeBetweenRequests, _lastRequestDuration / 2);
}
/// Sets the last request's result, either to success or failure.
/// And marks the last response time.
void setLastRequestResult(bool success)
{
markLastResponseTime();
if (success)
{
_lastRequestFailureCount = 0;
}
else
{
++_lastRequestFailureCount;
}
}
/// Indicates whether the last request was successful or not.
bool lastRequestSuccessful() const { return _lastRequestFailureCount == 0; }
/// Returns the number of failures in the previous requests. 0 for success.
std::size_t lastRequestFailureCount() const { return _lastRequestFailureCount; }
/// Helper to get the current time.
static std::chrono::steady_clock::time_point now()
{
return std::chrono::steady_clock::now();
}
private:
/// The minimum time between requests.
const std::chrono::milliseconds _minTimeBetweenRequests;
/// The last time we started an a request.
std::chrono::steady_clock::time_point _lastRequestTime;
/// The last time we received a response.
std::chrono::steady_clock::time_point _lastResponseTime;
/// The time we spent in the last request.
std::chrono::milliseconds _lastRequestDuration;
/// Counts the number of previous requests that failed.
/// Note that this is interpreted by the request in question.
/// For example, Core's Save operation turns 'false' for success
/// when the file is unmodified, but that is still a successful result.
std::size_t _lastRequestFailureCount;
};
/// Responsible for managing document saving.
/// Tracks idle-saving and its interval.
/// Tracks auto-saving and its interval.
/// Tracks the last save request and response times.
/// Tracks the local file's last modified time.
/// Tracks the time a save response was received.
class SaveManager final
{
/// Decide the auto-save interval. Returns 0 when disabled,
/// otherwise, the minimum of idle- and auto-save.
static std::chrono::milliseconds
getCheckInterval(std::chrono::milliseconds idleSaveInterval,
std::chrono::milliseconds autoSaveInterval)
{
if (idleSaveInterval > idleSaveInterval.zero())
{
if (autoSaveInterval > autoSaveInterval.zero())
return std::min(idleSaveInterval, autoSaveInterval);
return idleSaveInterval; // It's the only non-zero of the two.
}
return autoSaveInterval; // Regardless of whether it's 0 or not.
}
public:
SaveManager(std::chrono::milliseconds idleSaveInterval,
std::chrono::milliseconds autoSaveInterval,
std::chrono::milliseconds minTimeBetweenSaves)
: _request(minTimeBetweenSaves)