[Libreoffice-commits] online.git: Branch 'distro/cib/libreoffice-6-3' - 34 commits - common/Util.cpp common/Util.hpp configure.ac docker/Dockerfile docker/l10n-docker-nightly.sh docker/scripts kit/ForKit.cpp loleaflet/admin loleaflet/css loleaflet/html loleaflet/images loleaflet/Makefile.am loleaflet/reference.html loleaflet/src loolwsd.spec.in loolwsd.xml.in Makefile.am scripts/unocommands.py wsd/Admin.cpp wsd/Admin.hpp wsd/AdminModel.cpp wsd/AdminModel.hpp wsd/ClientSession.cpp wsd/ClientSession.hpp wsd/DocumentBroker.cpp wsd/DocumentBroker.hpp wsd/FileServer.cpp wsd/LOOLWSD.cpp wsd/LOOLWSD.hpp wsd/metrics.txt wsd/Storage.cpp wsd/Storage.hpp

Michael Meeks (via logerrit) logerrit at kemper.freedesktop.org
Tue Dec 17 07:10:47 UTC 2019


 Makefile.am                                |    3 
 common/Util.cpp                            |   25 +-
 common/Util.hpp                            |    3 
 configure.ac                               |   14 +
 docker/Dockerfile                          |    2 
 docker/l10n-docker-nightly.sh              |    2 
 docker/scripts/run-lool.sh                 |    4 
 kit/ForKit.cpp                             |    1 
 loleaflet/Makefile.am                      |    7 
 loleaflet/admin/admin.html                 |    4 
 loleaflet/admin/admin.strings.js           |    1 
 loleaflet/admin/src/AdminSocketOverview.js |    7 
 loleaflet/css/toolbar.css                  |    1 
 loleaflet/html/framed.doc.html             |  114 +++++++++--
 loleaflet/images/lc_insertfootnote.svg     |    1 
 loleaflet/reference.html                   |  155 +++++++++++++++
 loleaflet/src/control/Control.Menubar.js   |   80 +++++++-
 loleaflet/src/control/Control.Toolbar.js   |  226 +++++++++++-----------
 loleaflet/src/control/Ruler.js             |    1 
 loleaflet/src/control/Toolbar.js           |   14 +
 loleaflet/src/core/Socket.js               |    6 
 loleaflet/src/layer/tile/TileLayer.js      |    4 
 loleaflet/src/main.js                      |    4 
 loleaflet/src/map/Map.js                   |   28 ++
 loleaflet/src/map/handler/Map.Keyboard.js  |    9 
 loleaflet/src/map/handler/Map.WOPI.js      |  155 +++++++++++----
 loolwsd.spec.in                            |    1 
 loolwsd.xml.in                             |    5 
 scripts/unocommands.py                     |    2 
 wsd/Admin.cpp                              |   50 ++++-
 wsd/Admin.hpp                              |   11 +
 wsd/AdminModel.cpp                         |  289 ++++++++++++++++++++++++++++-
 wsd/AdminModel.hpp                         |   41 ++++
 wsd/ClientSession.cpp                      |   39 ++-
 wsd/ClientSession.hpp                      |    6 
 wsd/DocumentBroker.cpp                     |   29 ++
 wsd/DocumentBroker.hpp                     |    5 
 wsd/FileServer.cpp                         |    5 
 wsd/LOOLWSD.cpp                            |  102 +++++++---
 wsd/LOOLWSD.hpp                            |    2 
 wsd/Storage.cpp                            |    5 
 wsd/Storage.hpp                            |   11 +
 wsd/metrics.txt                            |  155 +++++++++++++++
 43 files changed, 1361 insertions(+), 268 deletions(-)

New commits:
commit 28e5004bceda40308106335f54b247a6929a2f54
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Mon Nov 25 12:06:07 2019 +0000
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Mon Dec 16 17:04:05 2019 +0100

    Add quick link to check monitoring.
    
    Change-Id: I069eeaf8464661aaff52f11523d70c40cc81cc46
    (cherry picked from commit 918d4ae150be391eedc70c4e193265d3b07ac23d)

diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index bdee5cb60..2cb9b4f26 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -17,6 +17,9 @@
 /* Default loleaflet UI used in the admin console URI */
 #define LOOLWSD_TEST_ADMIN_CONSOLE "/loleaflet/dist/admin/admin.html"
 
+/* Default loleaflet UI used in for monitoring URI */
+#define LOOLWSD_TEST_METRICS "/lool/getMetrics"
+
 /* Default loleaflet UI used in the start test URI */
 #define LOOLWSD_TEST_LOLEAFLET_UI "/loleaflet/" LOOLWSD_VERSION_HASH "/loleaflet.html"
 
@@ -628,12 +631,23 @@ namespace
 {
 
 #if ENABLE_DEBUG
-inline std::string getLaunchBase(const std::string &credentials)
+inline std::string getLaunchBase(bool asAdmin = false)
 {
     std::ostringstream oss;
     oss << "    ";
     oss << ((LOOLWSD::isSSLEnabled() || LOOLWSD::isSSLTermination()) ? "https://" : "http://");
-    oss << credentials;
+
+    if (asAdmin)
+    {
+        auto user = LOOLWSD::getConfigValue<std::string>("admin_console.username", "");
+        auto passwd = LOOLWSD::getConfigValue<std::string>("admin_console.password", "");
+
+        if (user.empty() || passwd.empty())
+            return "";
+
+        oss << user << ":" << passwd << "@";
+    }
+
     oss << LOOLWSD_TEST_HOST ":";
     oss << ClientPortNumber;
 
@@ -644,7 +658,7 @@ inline std::string getLaunchURI(const std::string &document)
 {
     std::ostringstream oss;
 
-    oss << getLaunchBase("");
+    oss << getLaunchBase();
     oss << LOOLWSD::ServiceRoot;
     oss << LOOLWSD_TEST_LOLEAFLET_UI;
     oss << "?file_path=file://";
@@ -654,33 +668,17 @@ inline std::string getLaunchURI(const std::string &document)
     return oss.str();
 }
 
-inline std::string getServiceURI(const std::string &sub)
+inline std::string getServiceURI(const std::string &sub, bool asAdmin = false)
 {
     std::ostringstream oss;
 
-    oss << getLaunchBase("");
+    oss << getLaunchBase(asAdmin);
     oss << LOOLWSD::ServiceRoot;
     oss << sub;
 
     return oss.str();
 }
 
-inline std::string getAdminURI(const Poco::Util::LayeredConfiguration &config)
-{
-    std::string user = config.getString("admin_console.username", "");
-    std::string passwd = config.getString("admin_console.password", "");
-
-    if (user.empty() || passwd.empty())
-        return "";
-
-    std::ostringstream oss;
-
-    oss << getLaunchBase(user + ":" + passwd + "@");
-    oss << LOOLWSD::ServiceRoot;
-    oss << LOOLWSD_TEST_ADMIN_CONSOLE;
-
-    return oss.str();
-}
 #endif
 
 } // anonymous namespace
@@ -1177,10 +1175,11 @@ void LOOLWSD::initialize(Application& self)
               << "    Calc:    " << getLaunchURI(LOOLWSD_TEST_DOCUMENT_RELATIVE_PATH_CALC) << '\n'
               << "    Impress: " << getLaunchURI(LOOLWSD_TEST_DOCUMENT_RELATIVE_PATH_IMPRESS) << std::endl;
 
-    const std::string adminURI = getAdminURI(config());
+    const std::string adminURI = getServiceURI(LOOLWSD_TEST_ADMIN_CONSOLE, true);
     if (!adminURI.empty())
-        std::cerr << "\nOr for the admin, capabilities & discovery:\n\n"
+        std::cerr << "\nOr for the admin, monitoring, capabilities & discovery:\n\n"
                   << adminURI << "\n"
+                  << getServiceURI(LOOLWSD_TEST_METRICS, true) << "\n"
                   << getServiceURI("/hosting/capabilities") << "\n"
                   << getServiceURI("/hosting/discovery") << "\n";
 
@@ -3161,13 +3160,7 @@ static LOOLWSDServer srv;
 #if ENABLE_DEBUG
 std::string LOOLWSD::getServerURL()
 {
-    std::ostringstream oss;
-
-    oss << getLaunchBase("");
-    oss << LOOLWSD::ServiceRoot;
-    oss << LOOLWSD_TEST_LOLEAFLET_UI;
-
-    return oss.str();
+    return getServiceURI(LOOLWSD_TEST_LOLEAFLET_UI);
 }
 #endif
 #endif
commit 7d3922fed5fafd5d5fc35b69ac3476e22290aef4
Author:     Gabriel Masei <gabriel.masei at 1and1.ro>
AuthorDate: Tue Nov 12 11:50:33 2019 +0200
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Mon Dec 16 16:55:17 2019 +0100

    Add REST endpoint for admin metrics.
    
    Change-Id: I701485631931334d27594c4907cb770f9888e5bf
    Reviewed-on: https://gerrit.libreoffice.org/82492
    Reviewed-by: Michael Meeks <michael.meeks at collabora.com>
    Tested-by: Michael Meeks <michael.meeks at collabora.com>
    (cherry picked from commit 2164f5207c1717173edcd462433bfa0ee3257045)

diff --git a/Makefile.am b/Makefile.am
index 9df919489..1b9dd4d35 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -29,7 +29,8 @@ man_MANS = man/loolwsd.1 \
 dist_doc_DATA = wsd/README \
                 wsd/README.vars \
                 wsd/protocol.txt \
-                wsd/reference.md
+                wsd/reference.md \
+                wsd/metrics.txt
 
 loolwsddatadir = @LOOLWSD_DATADIR@
 
diff --git a/common/Util.cpp b/common/Util.cpp
index 9185e04c4..d052f190a 100644
--- a/common/Util.cpp
+++ b/common/Util.cpp
@@ -173,7 +173,7 @@ namespace Util
     }
 
     // close what we have - far faster than going up to a 1m open_max eg.
-    static bool closeFdsFromProc()
+    static bool closeFdsFromProc(std::map<int, int> *mapFdsToKeep = nullptr)
     {
           DIR *fdDir = opendir("/proc/self/fd");
           if (!fdDir)
@@ -197,6 +197,9 @@ namespace Util
               if (fd < 3)
                   continue;
 
+              if (mapFdsToKeep && mapFdsToKeep->find(fd) != mapFdsToKeep->end())
+                  continue;
+
               if (close(fd) < 0)
                   std::cerr << "Unexpected failure to close fd " << fd << std::endl;
           }
@@ -205,22 +208,23 @@ namespace Util
           return true;
     }
 
-    static void closeFds()
+    static void closeFds(std::map<int, int> *mapFdsToKeep = nullptr)
     {
-        if (!closeFdsFromProc())
+        if (!closeFdsFromProc(mapFdsToKeep))
         {
             std::cerr << "Couldn't close fds efficiently from /proc" << std::endl;
             for (int fd = 3; fd < sysconf(_SC_OPEN_MAX); ++fd)
-                close(fd);
+                if (mapFdsToKeep->find(fd) != mapFdsToKeep->end())
+                    close(fd);
         }
     }
 
-    int spawnProcess(const std::string &cmd, const std::vector<std::string> &args, int *stdInput)
+    int spawnProcess(const std::string &cmd, const std::vector<std::string> &args, const std::vector<int>* fdsToKeep, int *stdInput)
     {
         int pipeFds[2] = { -1, -1 };
         if (stdInput)
         {
-            if (pipe(pipeFds) < 0)
+            if (pipe2(pipeFds, O_NONBLOCK) < 0)
             {
                 LOG_ERR("Out of file descriptors spawning " << cmd);
                 throw Poco::SystemException("Out of file descriptors");
@@ -233,6 +237,12 @@ namespace Util
             params.push_back(const_cast<char *>(i.c_str()));
         params.push_back(nullptr);
 
+        std::map<int, int> mapFdsToKeep;
+
+        if (fdsToKeep)
+            for (const auto& i : *fdsToKeep)
+                mapFdsToKeep[i] = i;
+
         int pid = fork();
         if (pid < 0)
         {
@@ -244,7 +254,7 @@ namespace Util
             if (stdInput)
                 dup2(pipeFds[0], STDIN_FILENO);
 
-            closeFds();
+            closeFds(&mapFdsToKeep);
 
             int ret = execvp(params[0], &params[0]);
             if (ret < 0)
@@ -260,6 +270,7 @@ namespace Util
         }
         return pid;
     }
+
 #endif
 
     bool dataFromHexString(const std::string& hexString, std::vector<unsigned char>& data)
diff --git a/common/Util.hpp b/common/Util.hpp
index e6e426ed0..6e74db78f 100644
--- a/common/Util.hpp
+++ b/common/Util.hpp
@@ -64,7 +64,8 @@ namespace Util
     /// Spawn a process if stdInput is non-NULL it contains a writable descriptor
     /// to send data to the child.
     int spawnProcess(const std::string &cmd, const std::vector<std::string> &args,
-                     int *stdInput = nullptr);
+                     const std::vector<int>* fdsToKeep = nullptr, int *stdInput = nullptr);
+    
 #endif
 
     /// Hex to unsigned char
diff --git a/kit/ForKit.cpp b/kit/ForKit.cpp
index bce34780f..ea887fe19 100644
--- a/kit/ForKit.cpp
+++ b/kit/ForKit.cpp
@@ -212,6 +212,7 @@ static void cleanupChildren()
     std::vector<std::string> jails;
     Process::PID exitedChildPid;
     int status;
+
     // Reap quickly without doing slow cleanup so WSD can spawn more rapidly.
     while ((exitedChildPid = waitpid(-1, &status, WUNTRACED | WNOHANG)) > 0)
     {
diff --git a/loolwsd.spec.in b/loolwsd.spec.in
index 790e12f67..d78760e82 100644
--- a/loolwsd.spec.in
+++ b/loolwsd.spec.in
@@ -106,6 +106,7 @@ echo "account    required     pam_unix.so" >>  %{buildroot}/etc/pam.d/loolwsd
 /usr/share/doc/loolwsd/README.vars
 /usr/share/doc/loolwsd/protocol.txt
 /usr/share/doc/loolwsd/reference.md
+/usr/share/doc/loolwsd/metrics.txt
 /usr/share/man/man1/loolwsd.1
 /usr/share/man/man1/loolforkit.1
 /usr/share/man/man1/loolconvert.1
diff --git a/wsd/Admin.cpp b/wsd/Admin.cpp
index 9d9fc2548..1f3d54878 100644
--- a/wsd/Admin.cpp
+++ b/wsd/Admin.cpp
@@ -507,7 +507,7 @@ size_t Admin::getTotalMemoryUsage()
     // memory to the forkit; and then count only dirty pages in the clients
     // since we know that they share everything else with the forkit.
     const size_t forkitRssKb = Util::getMemoryUsageRSS(_forKitPid);
-    const size_t wsdPssKb = Util::getMemoryUsagePSS(Poco::Process::id());
+    const size_t wsdPssKb = Util::getMemoryUsagePSS(getpid());
     const size_t kitsDirtyKb = _model.getKitsMemoryUsage();
     const size_t totalMem = wsdPssKb + forkitRssKb + kitsDirtyKb;
 
@@ -517,7 +517,7 @@ size_t Admin::getTotalMemoryUsage()
 size_t Admin::getTotalCpuUsage()
 {
     const size_t forkitJ = Util::getCpuUsage(_forKitPid);
-    const size_t wsdJ = Util::getCpuUsage(Poco::Process::id());
+    const size_t wsdJ = Util::getCpuUsage(getpid());
     const size_t kitsJ = _model.getKitsJiffies();
 
     if (_lastJiffies == 0)
@@ -567,6 +567,21 @@ void Admin::addBytes(const std::string& docKey, uint64_t sent, uint64_t recv)
     addCallback([=] { _model.addBytes(docKey, sent, recv); });
 }
 
+void Admin::setViewLoadDuration(const std::string& docKey, const std::string& sessionId, std::chrono::milliseconds viewLoadDuration)
+{
+    addCallback([=]{ _model.setViewLoadDuration(docKey, sessionId, viewLoadDuration); });
+}
+
+void Admin::setDocWopiDownloadDuration(const std::string& docKey, std::chrono::milliseconds wopiDownloadDuration)
+{
+    addCallback([=]{ _model.setDocWopiDownloadDuration(docKey, wopiDownloadDuration); });
+}
+
+void Admin::setDocWopiUploadDuration(const std::string& docKey, const std::chrono::milliseconds uploadDuration)
+{
+    addCallback([=]{ _model.setDocWopiUploadDuration(docKey, uploadDuration); });
+}
+
 void Admin::notifyForkit()
 {
     std::ostringstream oss;
@@ -691,6 +706,34 @@ void Admin::scheduleMonitorConnect(const std::string &uri, std::chrono::steady_c
     _pendingConnects.push_back(todo);
 }
 
+void Admin::getMetrics(std::ostringstream &metrics)
+{
+    size_t memAvail =  getTotalAvailableMemory();
+    size_t memUsed = getTotalMemoryUsage();
+
+    metrics << "global_host_system_memory_bytes " << _totalSysMemKb * 1024 << std::endl;
+    metrics << "global_memory_available_bytes " << memAvail * 1024 << std::endl;
+    metrics << "global_memory_used_bytes " << memUsed * 1024 << std::endl;
+    metrics << "global_memory_free_bytes " << (memAvail - memUsed) * 1024 << std::endl;
+    metrics << std::endl;
+
+    _model.getMetrics(metrics);
+}
+
+void Admin::sendMetrics(const std::shared_ptr<StreamSocket>& socket, const std::shared_ptr<Poco::Net::HTTPResponse>& response)
+{
+    std::ostringstream oss;
+    response->write(oss);
+    getMetrics(oss);
+    socket->send(oss.str());
+    socket->shutdown();
+}
+
+void Admin::sendMetricsAsync(const std::shared_ptr<StreamSocket>& socket, const std::shared_ptr<Poco::Net::HTTPResponse>& response)
+{
+    addCallback([this, socket, response]{ sendMetrics(socket, response); });
+}
+
 void Admin::start()
 {
     bool haveMonitors = false;
diff --git a/wsd/Admin.hpp b/wsd/Admin.hpp
index f29ea2c4f..2157def98 100644
--- a/wsd/Admin.hpp
+++ b/wsd/Admin.hpp
@@ -90,7 +90,7 @@ public:
     /// Remove the document with all views. Used on termination or catastrophic failure.
     void rmDoc(const std::string& docKey);
 
-    void setForKitPid(const int forKitPid) { _forKitPid = forKitPid; }
+    void setForKitPid(const int forKitPid) { _forKitPid = forKitPid; _model.setForKitPid(forKitPid);}
     void setForKitWritePipe(const int forKitWritePipe) { _forKitWritePipe = forKitWritePipe; }
 
     /// Callers must ensure that modelMutex is acquired
@@ -123,6 +123,15 @@ public:
     /// Attempt a synchronous connection to a monitor with @uri @when that future comes
     void scheduleMonitorConnect(const std::string &uri, std::chrono::steady_clock::time_point when);
 
+    void sendMetrics(const std::shared_ptr<StreamSocket>& socket, const std::shared_ptr<Poco::Net::HTTPResponse>& response);
+    void sendMetricsAsync(const std::shared_ptr<StreamSocket>& socket, const std::shared_ptr<Poco::Net::HTTPResponse>& response);
+
+    void setViewLoadDuration(const std::string& docKey, const std::string& sessionId, std::chrono::milliseconds viewLoadDuration);
+    void setDocWopiDownloadDuration(const std::string& docKey, std::chrono::milliseconds wopiDownloadDuration);
+    void setDocWopiUploadDuration(const std::string& docKey, const std::chrono::milliseconds uploadDuration);
+
+    void getMetrics(std::ostringstream &metrics);
+
 private:
     /// Notify Forkit of changed settings.
     void notifyForkit();
diff --git a/wsd/AdminModel.cpp b/wsd/AdminModel.cpp
index e0074ce5f..5913b1c5d 100644
--- a/wsd/AdminModel.cpp
+++ b/wsd/AdminModel.cpp
@@ -13,6 +13,7 @@
 
 #include <chrono>
 #include <memory>
+#include <regex>
 #include <set>
 #include <sstream>
 #include <string>
@@ -28,6 +29,9 @@
 #include <Util.hpp>
 #include <wsd/LOOLWSD.hpp>
 
+#include <fnmatch.h>
+#include <dirent.h>
+
 void Document::addView(const std::string& sessionId, const std::string& userName, const std::string& userId)
 {
     const auto ret = _views.emplace(sessionId, View(sessionId, userName, userId));
@@ -57,6 +61,13 @@ int Document::expireView(const std::string& sessionId)
     return _activeViews;
 }
 
+void Document::setViewLoadDuration(const std::string& sessionId, std::chrono::milliseconds viewLoadDuration)
+{
+    std::map<std::string, View>::iterator it = _views.find(sessionId);
+    if (it != _views.end())
+        it->second.setLoadDuration(viewLoadDuration);
+}
+
 std::pair<std::time_t, std::string> Document::getSnapshot() const
 {
     std::time_t ct = std::time(nullptr);
@@ -530,7 +541,7 @@ void AdminModel::removeDocument(const std::string& docKey, const std::string& se
         // to the admin console with views.
         if (docIt->second.expireView(sessionId) == 0)
         {
-            _expiredDocuments.emplace(*docIt);
+            _expiredDocuments.emplace(docIt->first + std::to_string(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count()), docIt->second);
             _documents.erase(docIt);
         }
     }
@@ -556,7 +567,7 @@ void AdminModel::removeDocument(const std::string& docKey)
         }
 
         LOG_DBG("Removed admin document [" << docKey << "].");
-        _expiredDocuments.emplace(*docIt);
+        _expiredDocuments.emplace(docIt->first + std::to_string(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count()), docIt->second);
         _documents.erase(docIt);
     }
 }
@@ -741,4 +752,270 @@ double AdminModel::getServerUptime()
     return uptime.count();
 }
 
+void AdminModel::setViewLoadDuration(const std::string& docKey, const std::string& sessionId, std::chrono::milliseconds viewLoadDuration)
+{
+    std::map<std::string, Document>::iterator it = _documents.find(docKey);
+    if (it != _documents.end())
+        it->second.setViewLoadDuration(sessionId, viewLoadDuration);
+}
+
+void AdminModel::setDocWopiDownloadDuration(const std::string& docKey, std::chrono::milliseconds wopiDownloadDuration)
+{
+    std::map<std::string, Document>::iterator it = _documents.find(docKey);
+    if (it != _documents.end())
+        it->second.setWopiDownloadDuration(wopiDownloadDuration);
+}
+
+void AdminModel::setDocWopiUploadDuration(const std::string& docKey, const std::chrono::milliseconds wopiUploadDuration)
+{
+    std::map<std::string, Document>::iterator it = _documents.find(docKey);
+    if (it != _documents.end())
+        it->second.setWopiUploadDuration(wopiUploadDuration);
+}
+
+int filterNumberName(const struct dirent *dir)
+{
+    return !fnmatch("[0-9]*", dir->d_name, 0);
+}
+
+int AdminModel::getPidsFromProcName(const std::regex& procNameRegEx, std::vector<int> *pids)
+{
+    struct dirent **namelist = NULL;
+    int n = scandir("/proc", &namelist, filterNumberName, 0);
+    int pidCount = 0;
+
+    if (n < 0)
+        return n;
+
+    std::string comm;
+    char line[256] = { 0 }; //Here we need only 16 bytes but for safety reasons we use file name max length
+
+    if (pids != NULL)
+        pids->clear();
+
+    while (n--)
+    {
+        comm = "/proc/";
+        comm += namelist[n]->d_name;
+        comm += "/comm";
+        FILE* fp = fopen(comm.c_str(), "r");
+        if (fp != nullptr)
+        {
+            if (fgets(line, sizeof (line), fp))
+            {
+                char *nl = strchr(line, '\n');
+                if (nl != NULL)
+                    *nl = 0;
+                if (regex_match(line, procNameRegEx))
+                {
+                    pidCount ++;
+                    if (pids)
+                        pids->push_back(strtol(namelist[n]->d_name, NULL, 10));
+                }
+            }
+            fclose(fp);
+        }
+        free(namelist[n]);
+    }
+    free(namelist);
+
+    return pidCount;
+}
+
+class AggregateStats
+{
+public:
+    AggregateStats()
+    : _total(0), _min(0xFFFFFFFFFFFFFFFF), _max(0), _count(0)
+    {}
+
+    void Update(uint64_t value)
+    {
+        _total += value;
+        _min = (_min > value ? value : _min);
+        _max = (_max < value ? value : _max);
+        _count ++;
+    }
+
+    uint64_t getIntAverage() const { return _count ? std::round(_total / (double)_count) : 0; }
+    double getDoubleAverage() const { return _count ? _total / (double) _count : 0; }
+    uint64_t getMin() const { return _min == 0xFFFFFFFFFFFFFFFF ? 0 : _min; }
+    uint64_t getMax() const { return _max; }
+    uint64_t getTotal() const { return _total; }
+    uint64_t getCount() const { return _count; }
+
+    void Print(std::ostringstream &oss, const char *prefix, const char* unit) const
+    {
+        std::string newUnit = std::string(unit && unit[0] ? "_" : "") + unit;
+        std::string newPrefix = prefix + std::string(prefix && prefix[0] ? "_" : "");
+
+        oss << newPrefix << "total" << newUnit << " " << _total << std::endl;
+        oss << newPrefix << "average" << newUnit << " " << getIntAverage() << std::endl;
+        oss << newPrefix << "min" << newUnit << " " << getMin() << std::endl;
+        oss << newPrefix << "max" << newUnit << " " << _max << std::endl;
+    }
+
+private:
+    uint64_t _total;
+    uint64_t _min;
+    uint64_t _max;
+    uint32_t _count;
+};
+
+struct ActiveExpiredStats
+{
+public:
+
+    void Update(uint64_t value, bool active)
+    {
+        _all.Update(value);
+        if (active)
+            _active.Update(value);
+        else
+            _expired.Update(value);
+    }
+
+    void Print(std::ostringstream &oss, const char *prefix, const char* name, const char* unit) const
+    {
+        std::ostringstream ossTmp;
+        std::string newName = std::string(name && name[0] ? "_" : "") + name;
+        std::string newPrefix = prefix + std::string(prefix && prefix[0] ? "_" : "");
+
+        ossTmp << newPrefix << "all" << newName;
+        _all.Print(oss, ossTmp.str().c_str(), unit);
+        ossTmp.str(std::string());
+        ossTmp << newPrefix << "active" << newName;
+        _active.Print(oss, ossTmp.str().c_str(), unit);
+        ossTmp.str(std::string());
+        ossTmp << newPrefix << "expired" << newName;
+        _expired.Print(oss, ossTmp.str().c_str(), unit);
+    }
+
+    AggregateStats _all;
+    AggregateStats _active;
+    AggregateStats _expired;
+};
+
+struct DocumentAggregateStats
+{
+    void Update(const Document &d, bool active)
+    {
+        _kitUsedMemory.Update(d.getMemoryDirty(), active);
+        _viewsCount.Update(d.getViews().size(), active);
+        _activeViewsCount.Update(d.getActiveViews(), active);
+        _expiredViewsCount.Update(d.getViews().size() - d.getActiveViews(), active);
+        _openedTime.Update(d.getOpenTime(), active);
+        _bytesSentToClients.Update(d.getSentBytes(), active);
+        _bytesRecvFromClients.Update(d.getRecvBytes(), active);
+        _wopiDownloadDuration.Update(d.getWopiDownloadDuration().count(), active);
+        _wopiUploadDuration.Update(d.getWopiUploadDuration().count(), active);
+
+        //View load duration
+        for (const auto& v : d.getViews())
+            _viewLoadDuration.Update(v.second.getLoadDuration().count(), active);
+    }
+
+    ActiveExpiredStats _kitUsedMemory;
+    ActiveExpiredStats _viewsCount;
+    ActiveExpiredStats _activeViewsCount;
+    ActiveExpiredStats _expiredViewsCount;
+    ActiveExpiredStats _openedTime;
+    ActiveExpiredStats _bytesSentToClients;
+    ActiveExpiredStats _bytesRecvFromClients;
+    ActiveExpiredStats _wopiDownloadDuration;
+    ActiveExpiredStats _wopiUploadDuration;
+    ActiveExpiredStats _viewLoadDuration;
+};
+
+struct KitProcStats
+{
+    void UpdateAggregateStats(int pid)
+    {
+        _threadCount.Update(Util::getStatFromPid(pid, 19));
+        _cpuTime.Update(Util::getCpuUsage(pid));
+    }
+
+    int unassignedCount;
+    int assignedCount;
+    AggregateStats _threadCount;
+    AggregateStats _cpuTime;
+};
+
+void AdminModel::CalcDocAggregateStats(DocumentAggregateStats& stats)
+{
+    for (auto& d : _documents)
+        stats.Update(d.second, true);
+
+    for (auto& d : _expiredDocuments)
+        stats.Update(d.second, false);
+}
+
+void CalcKitStats(KitProcStats& stats)
+{
+    std::vector<int> childProcs;
+    stats.unassignedCount = AdminModel::getPidsFromProcName(std::regex("kit_spare_[0-9]*"), &childProcs);
+    stats.assignedCount = AdminModel::getPidsFromProcName(std::regex("kitbroker_[0-9]*"), &childProcs);
+    for (int& pid : childProcs)
+    {
+        stats.UpdateAggregateStats(pid);
+    }
+}
+
+void PrintDocActExpMetrics(std::ostringstream &oss, const char* name, const char* unit, const ActiveExpiredStats &values)
+{
+    values.Print(oss, "document", name, unit);
+}
+
+void PrintKitAggregateMetrics(std::ostringstream &oss, const char* name, const char* unit, const AggregateStats &values)
+{
+    std::string prefix = std::string("kit_") + name;
+    values.Print(oss, prefix.c_str(), unit);
+}
+
+void AdminModel::getMetrics(std::ostringstream &oss)
+{
+    oss << "loolwsd_count " << getPidsFromProcName(std::regex("loolwsd"), nullptr) << std::endl;
+    oss << "loolwsd_thread_count " << Util::getStatFromPid(getpid(), 19) << std::endl;
+    oss << "loolwsd_cpu_time_seconds " << Util::getCpuUsage(getpid()) / sysconf (_SC_CLK_TCK) << std::endl;
+    oss << "loolwsd_memory_used_bytes " << Util::getMemoryUsagePSS(getpid()) * 1024 << std::endl;
+    oss << std::endl;
+
+    oss << "forkit_count " << getPidsFromProcName(std::regex("forkit"), nullptr) << std::endl;
+    oss << "forkit_thread_count " << Util::getStatFromPid(_forKitPid, 19) << std::endl;
+    oss << "forkit_cpu_time_seconds " << Util::getCpuUsage(_forKitPid) / sysconf (_SC_CLK_TCK) << std::endl;
+    oss << "forkit_memory_used_bytes " << Util::getMemoryUsageRSS(_forKitPid) * 1024 << std::endl;
+    oss << std::endl;
+
+    DocumentAggregateStats docStats;
+    KitProcStats kitStats;
+
+    CalcDocAggregateStats(docStats);
+    CalcKitStats(kitStats);
+
+    oss << "kit_count " << kitStats.unassignedCount + kitStats.assignedCount << std::endl;
+    oss << "kit_unassigned_count " << kitStats.unassignedCount << std::endl;
+    oss << "kit_assigned_count " << kitStats.assignedCount << std::endl;
+    PrintKitAggregateMetrics(oss, "thread_count", "", kitStats._threadCount);
+    PrintKitAggregateMetrics(oss, "memory_used", "bytes", docStats._kitUsedMemory._all);
+    PrintKitAggregateMetrics(oss, "cpu_time", "seconds", kitStats._cpuTime);
+    oss << std::endl;
+
+    PrintDocActExpMetrics(oss, "views_all_count", "", docStats._viewsCount);
+    docStats._activeViewsCount._active.Print(oss, "document_active_views_active_count", "");
+    docStats._expiredViewsCount._active.Print(oss, "document_active_views_expired_count", "");
+    oss << std::endl;
+
+    PrintDocActExpMetrics(oss, "opened_time", "seconds", docStats._openedTime);
+    oss << std::endl;
+    PrintDocActExpMetrics(oss, "sent_to_clients", "bytes", docStats._bytesSentToClients);
+    oss << std::endl;
+    PrintDocActExpMetrics(oss, "received_from_client", "bytes", docStats._bytesRecvFromClients);
+    oss << std::endl;
+    PrintDocActExpMetrics(oss, "wopi_upload_duration", "milliseconds", docStats._wopiUploadDuration);
+    oss << std::endl;
+    PrintDocActExpMetrics(oss, "wopi_download_duration", "milliseconds", docStats._wopiDownloadDuration);
+    oss << std::endl;
+    PrintDocActExpMetrics(oss, "view_load_duration", "milliseconds", docStats._viewLoadDuration);
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/AdminModel.hpp b/wsd/AdminModel.hpp
index 57e7193e1..9e914f66d 100644
--- a/wsd/AdminModel.hpp
+++ b/wsd/AdminModel.hpp
@@ -11,8 +11,10 @@
 #define INCLUDED_ADMINMODEL_HPP
 
 #include <memory>
+#include <regex>
 #include <set>
 #include <string>
+#include <cmath>
 
 #include <Poco/Process.h>
 
@@ -20,6 +22,8 @@
 #include "net/WebSocketHandler.hpp"
 #include "Util.hpp"
 
+class DocumentAggregateStats;
+
 /// A client view in Admin controller.
 class View
 {
@@ -28,7 +32,8 @@ public:
         _sessionId(sessionId),
         _userName(userName),
         _userId(userId),
-        _start(std::time(nullptr))
+        _start(std::time(nullptr)),
+        _loadDuration(0)
     {
     }
 
@@ -37,6 +42,8 @@ public:
     std::string getUserId() const { return _userId; }
     std::string getSessionId() const { return _sessionId; }
     bool isExpired() const { return _end != 0 && std::time(nullptr) >= _end; }
+    std::chrono::milliseconds getLoadDuration() const { return _loadDuration; }
+    void setLoadDuration(std::chrono::milliseconds loadDuration) { _loadDuration = loadDuration; }
 
 private:
     const std::string _sessionId;
@@ -44,6 +51,7 @@ private:
     const std::string _userId;
     const std::time_t _start;
     std::time_t _end = 0;
+    std::chrono::milliseconds _loadDuration;
 };
 
 struct DocProcSettings
@@ -108,6 +116,8 @@ public:
           _end(0),
           _sentBytes(0),
           _recvBytes(0),
+          _wopiDownloadDuration(0),
+          _wopiUploadDuration(0),
           _isModified(false)
     {
     }
@@ -155,6 +165,15 @@ public:
     const DocProcSettings& getDocProcSettings() const { return _docProcSettings; }
     void setDocProcSettings(const DocProcSettings& docProcSettings) { _docProcSettings = docProcSettings; }
 
+    std::time_t getOpenTime() const { return isExpired() ? _end - _start : getElapsedTime(); }
+    uint64_t getSentBytes() const { return _sentBytes; }
+    uint64_t getRecvBytes() const { return _recvBytes; }
+    void setViewLoadDuration(const std::string& sessionId, std::chrono::milliseconds viewLoadDuration);
+    void setWopiDownloadDuration(std::chrono::milliseconds wopiDownloadDuration) { _wopiDownloadDuration = wopiDownloadDuration; }
+    std::chrono::milliseconds getWopiDownloadDuration() const { return _wopiDownloadDuration; }
+    void setWopiUploadDuration(const std::chrono::milliseconds wopiUploadDuration) { _wopiUploadDuration = wopiUploadDuration; }
+    std::chrono::milliseconds getWopiUploadDuration() const { return _wopiUploadDuration; }
+
     std::string to_string() const;
 
 private:
@@ -179,6 +198,10 @@ private:
     /// Total bytes sent and recv'd by this document.
     uint64_t _sentBytes, _recvBytes;
 
+    //Download/upload duration from/to storage for this document
+    std::chrono::milliseconds _wopiDownloadDuration;
+    std::chrono::milliseconds _wopiUploadDuration;
+
     /// Per-doc kit process settings.
     DocProcSettings _docProcSettings;
     bool _isModified;
@@ -224,6 +247,7 @@ private:
 class AdminModel
 {
 public:
+
     AdminModel() :
         _owner(std::this_thread::get_id())
     {
@@ -289,6 +313,15 @@ public:
     /// Document basic info list sorted by most idle time
     std::vector<DocBasicInfo> getDocumentsSortedByIdle() const;
 
+    void setViewLoadDuration(const std::string& docKey, const std::string& sessionId, std::chrono::milliseconds viewLoadDuration);
+    void setDocWopiDownloadDuration(const std::string& docKey, std::chrono::milliseconds wopiDownloadDuration);
+    void setDocWopiUploadDuration(const std::string& docKey, const std::chrono::milliseconds wopiUploadDuration);
+    void setForKitPid(pid_t pid) { _forKitPid = pid; }
+
+    void getMetrics(std::ostringstream &oss);
+
+    static int getPidsFromProcName(const std::regex& procNameRegEx, std::vector<int> *pids);
+
 private:
     std::string getMemStats();
 
@@ -302,6 +335,8 @@ private:
 
     std::string getDocuments() const;
 
+    void CalcDocAggregateStats(DocumentAggregateStats& stats);
+
 private:
     std::map<int, Subscriber> _subscribers;
     std::map<std::string, Document> _documents;
@@ -323,6 +358,8 @@ private:
     uint64_t _sentBytesTotal;
     uint64_t _recvBytesTotal;
 
+    pid_t _forKitPid;
+
     /// We check the owner even in the release builds, needs to be always correct.
     std::thread::id _owner;
 };
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 8ac41527e..65dc6d43c 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -440,6 +440,7 @@ bool ClientSession::loadDocument(const char* /*buffer*/, int /*length*/,
         return false;
     }
 
+    _viewLoadStart = std::chrono::steady_clock::now();
     LOG_INF("Requesting document load from child.");
     try
     {
@@ -970,6 +971,10 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         {
             setViewLoaded();
             docBroker->setLoaded();
+            
+#if !MOBILEAPP
+            Admin::instance().setViewLoadDuration(docBroker->getDocKey(), getId(), std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - _viewLoadStart));
+#endif
 
             for(auto &token : tokens)
             {
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 8db5b3718..ce75b27cb 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -223,6 +223,12 @@ private:
 
     /// Store wireID's of the sent tiles inside the actual visible area
     std::map<std::string, TileWireId> _oldWireIds;
+
+    /// Sockets to send binary selection content to
+    std::vector<std::weak_ptr<StreamSocket>> _clipSockets;
+
+    ///Time when loading of view started
+    std::chrono::steady_clock::time_point _viewLoadStart;
 };
 
 
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 9431e0fef..20c26f799 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -190,7 +190,8 @@ DocumentBroker::DocumentBroker(const std::string& uri,
     _stop(false),
     _closeReason("stopped"),
     _tileVersion(0),
-    _debugRenderedTileCount(0)
+    _debugRenderedTileCount(0),
+    _wopiLoadDuration(0)
 {
     assert(!_docKey.empty());
     assert(!LOOLWSD::ChildRoot.empty());
@@ -770,6 +771,7 @@ bool DocumentBroker::load(const std::shared_ptr<ClientSession>& session, const s
         auto callDuration = wopiStorage->getWopiLoadDuration();
         // Add the time taken to check file info
         callDuration += getInfoCallDuration;
+        _wopiLoadDuration = std::chrono::duration_cast<std::chrono::milliseconds>(callDuration);
         const std::string msg = "stats: wopiloadduration " + std::to_string(callDuration.count());
         LOG_TRC("Sending to Client [" << msg << "].");
         session->sendTextFrame(msg);
@@ -880,6 +882,12 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
     StorageBase::SaveResult storageSaveResult = _storage->saveLocalFileToStorage(auth, saveAsPath, saveAsFilename, isRename);
     if (storageSaveResult.getResult() == StorageBase::SaveResult::OK)
     {
+#if !MOBILEAPP
+        WopiStorage* wopiStorage = dynamic_cast<WopiStorage*>(_storage.get());
+        if (wopiStorage != nullptr)
+            Admin::instance().setDocWopiUploadDuration(_docKey, std::chrono::duration_cast<std::chrono::milliseconds>(wopiStorage->getWopiSaveDuration()));
+#endif
+
         if (!isSaveAs && !isRename)
         {
             // Saved and stored; update flags.
@@ -1192,6 +1200,7 @@ size_t DocumentBroker::addSessionInternal(const std::shared_ptr<ClientSession>&
 #if !MOBILEAPP
     // Tell the admin console about this new doc
     Admin::instance().addDoc(_docKey, getPid(), getFilename(), id, session->getUserName(), session->getUserId());
+    Admin::instance().setDocWopiDownloadDuration(_docKey, _wopiLoadDuration);
 #endif
 
     // Add and attach the session.
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index a31727d7a..91e8d14a4 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -33,6 +33,8 @@
 
 #include "common/SigUtil.hpp"
 
+#include "Admin.hpp"
+
 // Forwards.
 class PrisonerRequestDispatcher;
 class DocumentBroker;
@@ -466,6 +468,7 @@ private:
     std::chrono::steady_clock::time_point _lastActivityTime;
     std::chrono::steady_clock::time_point _threadStart;
     std::chrono::milliseconds _loadDuration;
+    std::chrono::milliseconds _wopiLoadDuration;
 
     /// Unique DocBroker ID for tracing and debugging.
     static std::atomic<unsigned> DocBrokerId;
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index f1e3d92fd..bdee5cb60 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -374,7 +374,7 @@ static int forkChildren(const int number)
 #else
         const std::string aMessage = "spawn " + std::to_string(number) + "\n";
         LOG_DBG("MasterToForKit: " << aMessage.substr(0, aMessage.length() - 1));
-        if (IoUtil::writeToPipe(LOOLWSD::ForKitWritePipe, aMessage) > 0)
+        if (write(LOOLWSD::ForKitWritePipe, aMessage.c_str(), aMessage.length()) > 0)
 #endif
         {
             OutstandingForks += number;
@@ -1635,7 +1635,7 @@ bool LOOLWSD::createForKit()
 
     LastForkRequestTime = std::chrono::steady_clock::now();
     int childStdin = -1;
-    int child = Util::spawnProcess(forKitPath, args, &childStdin);
+    int child = Util::spawnProcess(forKitPath, args, nullptr, &childStdin);
 
     ForKitWritePipe = childStdin;
     ForKitProcId = child;
@@ -2104,6 +2104,47 @@ private:
                 }
 
             }
+            else if (reqPathSegs.size() >= 2 && reqPathSegs[0] == "lool" && reqPathSegs[1] == "getMetrics")
+            {
+                //See metrics.txt
+                std::shared_ptr<Poco::Net::HTTPResponse> response(new Poco::Net::HTTPResponse());
+
+                if (!LOOLWSD::AdminEnabled)
+                    throw Poco::FileAccessDeniedException("Admin console disabled");
+
+                try{
+                    if (!FileServerRequestHandler::isAdminLoggedIn(request, *response.get()))
+                        throw Poco::Net::NotAuthenticatedException("Invalid admin login");
+                }
+                catch (const Poco::Net::NotAuthenticatedException& exc)
+                {
+                    //LOG_ERR("FileServerRequestHandler::NotAuthenticated: " << exc.displayText());
+                    std::ostringstream oss;
+                    oss << "HTTP/1.1 401 \r\n"
+                        << "Content-Type: text/html charset=UTF-8\r\n"
+                        << "Date: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n"
+                        << "User-Agent: " << WOPI_AGENT_STRING << "\r\n"
+                        << "WWW-authenticate: Basic realm=\"online\"\r\n"
+                        << "\r\n";
+                    socket->send(oss.str());
+                    socket->shutdown();
+                    return;
+                }
+
+                response->add("Last-Modified", Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT));
+                // Ask UAs to block if they detect any XSS attempt
+                response->add("X-XSS-Protection", "1; mode=block");
+                // No referrer-policy
+                response->add("Referrer-Policy", "no-referrer");
+                response->add("User-Agent", HTTP_AGENT_STRING);
+                response->add("Content-Type", "text/plain");
+                response->add("X-Content-Type-Options", "nosniff");
+
+                disposition.setMove([response](const std::shared_ptr<Socket> &moveSocket){
+                            const std::shared_ptr<StreamSocket> streamSocket = std::static_pointer_cast<StreamSocket>(moveSocket);
+                            Admin::instance().sendMetricsAsync(streamSocket, response);
+                        });
+            }
             // Client post and websocket connections
             else if ((request.getMethod() == HTTPRequest::HTTP_GET ||
                       request.getMethod() == HTTPRequest::HTTP_HEAD) &&
diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp
index dd26d7b38..52c18ed81 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -714,6 +714,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
     LOG_INF("Uploading URI via WOPI [" << uriAnonym << "] from [" << filePathAnonym + "].");
 
     StorageBase::SaveResult saveResult(StorageBase::SaveResult::FAILED);
+    const auto startTime = std::chrono::steady_clock::now();
     try
     {
         std::unique_ptr<Poco::Net::HTTPClientSession> psession(getHTTPClientSession(uriObject));
@@ -795,6 +796,8 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
         Poco::Net::HTTPResponse response;
         std::istream& rs = psession->receiveResponse(response);
 
+        _wopiSaveDuration = std::chrono::steady_clock::now() - startTime;
+
         std::ostringstream oss;
         Poco::StreamCopier::copyStream(rs, oss);
         std::string responseString = oss.str();
diff --git a/wsd/Storage.hpp b/wsd/Storage.hpp
index ed1c69a69..1aa6346d3 100644
--- a/wsd/Storage.hpp
+++ b/wsd/Storage.hpp
@@ -296,7 +296,8 @@ public:
                 const std::string& localStorePath,
                 const std::string& jailPath) :
         StorageBase(uri, localStorePath, jailPath),
-        _wopiLoadDuration(0)
+        _wopiLoadDuration(0),
+        _wopiSaveDuration(0)
     {
         LOG_INF("WopiStorage ctor with localStorePath: [" << localStorePath <<
                 "], jailPath: [" << jailPath << "], uri: [" << LOOLWSD::anonymizeUrl(uri.toString()) << "].");
@@ -492,10 +493,12 @@ public:
 
     /// Total time taken for making WOPI calls during load
     std::chrono::duration<double> getWopiLoadDuration() const { return _wopiLoadDuration; }
+    std::chrono::duration<double> getWopiSaveDuration() const { return _wopiSaveDuration; }
 
 private:
     // Time spend in loading the file from storage
     std::chrono::duration<double> _wopiLoadDuration;
+    std::chrono::duration<double> _wopiSaveDuration;
 };
 
 /// WebDAV protocol backed storage.
diff --git a/wsd/metrics.txt b/wsd/metrics.txt
new file mode 100644
index 000000000..f12c3fe64
--- /dev/null
+++ b/wsd/metrics.txt
@@ -0,0 +1,155 @@
+Below is the description of the metrics returned by 'getMetrics' REST endpoint.
+The general format of the output is complient with Prometheus text-based format
+which can be found here: https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
+
+GLOBAL
+
+    global_host_system_memory_bytes - Total host system memory in bytes.
+    global_memory_available_bytes – Memory available to our application in bytes. This is equal to global_host_system_memory_bytes * memproportion where memproportion represents the maximum percentage of system memory consumed by all of the LibreOffice Online, after which we start cleaning up idle documents. This parameter can be setup in loolwsd.xml.
+    global_memory_used_bytes – Total memory usage: PSS(loolwsd) + RSS(forkit) + Private_Dirty(all assigned loolkits).
+    global_memory_free_bytes - global_memory_available_bytes - global_memory_used_bytes
+
+LOOLWSD
+
+    loolwsd_count – number of running loolwsd processes.
+    loolwsd_thread_count – number of threads in the current loolwsd process.
+    loolwsd_cpu_time_seconds – the CPU usage by current loolwsd process.
+    loolwsd_memory_used_bytes – the memory used by current loolwsd process: PSS(loolwsd).
+
+FORKIT
+
+    forkit_process_count – number of running forkit processes.
+    forkit_thread_count – number of threads in the current forkit process.
+    forkit_cpu_time_seconds – the CPU usage by the current forkit process.
+    forkit_memory_used_bytes - the memory used by the current forkit process: RSS(forkit).
+
+KITS
+
+    kit_count – total number of running kit processes.
+    kit_unassigned_count – number of running kit processes that are not assigned to documents.
+    kit_assigned_count – number of running kit processes that are assigned to documents.
+    kit_thread_count_total - total number of threads in all running kit processes.
+    kit_thread_count_average – average number of threads per running kit process.
+    kit_thread_count_min - minimum from the number of threads in each running kit process.
+    kit_thread_count_max – maximum from the number of threads in each running kit process.
+    kit_memory_used_total_bytes – total Private_Dirty memory used by all running kit processes.
+    kit_memory_used_average_bytes – average between the Private_Dirty memory used by each active kit process.
+    kit_memory_used_min_bytes – minimum from the Private_Dirty memory used by each running kit process.
+    kit_memory_used_max_bytes - maximum from the Private_Dirty memory used by each running kit process.
+    kit_cpu_time_total_seconds – total CPU time for all running kit processes.
+    kit_cpu_time_average_seconds – average between the CPU time each running kit process used.
+    kit_cpu_time_min_seconds – minimum from the CPU time each running kit process used.
+    kit_cpu_time_max_seconds - maximum from the CPU time each running kit process used.
+
+DOCUMENT VIEWS
+
+    document_all_views_all_count_total - total number of views (active or expired) of all documents (active and expired).
+    document_all_views_all_count_average – average between the number of all views (active or expired) per document (active or expired).
+    document_all_views_all_count_min – minimum from the number of all views (active or expired) of each document (active or expired).
+    document_all_views_all_count_max – maximum from the number of all views (active or expired) of each document (active or expired).
+    document_active_views_all_count_total - total number of all views (active or expired) of active documents.
+    document_active_views_all_count_average – average between the number of all views (active or expired) of each active document.
+    document_active_views_all_count_min – minimum from the number of all views (active or expired) of each active document.
+    document_active_views_all_count_max – maximum from the number of all views (active or expired) of each active document.
+    document_expired_views_all_count_total - total number of all views (active or expired) of expired documents.
+    document_expired_views_all_count_average – average between the number of all views (active or expired) of each expired document.
+    document_expired_views_all_count_min – minimum from the number of all views (active or expired) of each expired document.
+    document_expired_views_all_count_max – maximum from the number of all views (active or expired) of each expired document.
+    document_active_views_active_count_total – total number of active views of all active documents.
+    document_active_views_active_count_average – average between the number of active views of each active document.
+    document_active_views_active_count_min – minimum from the number of active views of each active document.
+    document_active_views_active_count_max – maximum from the number of active views of each active document.
+    document_active_views_expired_count_total – total number of expired views of all active documents.
+    document_active_views_expired_count_average – average between the number of expired views of each active document.
+    document_active_views_expired_count_min – minimum from the number of expired views of each active document.
+    document_active_views_expired_count_max – maximum from the number of expired views of each active document.
+
+DOCUMENT OPENED TIME
+
+    document_all_opened_time_total_seconds - sum of time each document (active or expired) was kept opened.
+    document_all_opened_time_average_seconds – average between the time intervals each document (active or expired) was kept opened.
+    document_all_opened_time_min_seconds – minimum from the time intervals each document (active or expired) was kept opened.
+    document_all_opened_time_max_seconds - maximum from the time intervals each document (active or expired) was kept opened.
+    document_active_opened_time_total_seconds - sum of time each active document was kept opened.
+    document_active_opened_time_average_seconds – average between the time intervals each active document was kept opened.
+    document_active_opened_time_min_seconds - minimum from the time intervals each active document was kept opened.
+    document_active_opened_time_max_seconds - maximum from the time intervals each active document was kept opened.
+    document_expired_opened_time_total_seconds - sum of time each expired document was kept opened.
+    document_expired_opened_time_average_seconds – average between the time intervals each expired document was kept opened.
+    document_expired_opened_time_min_seconds - minimum from the time intervals each expired document was kept opened.
+    document_expired_opened_time_max_seconds – maximum from the time intervals each expired document was kept opened.
+
+DOCUMENT BYTES SENT TO CLIENTS
+
+    document_all_sent_to_clients_total_bytes - total number of bytes sent to clients by all documents (active or expired).
+    document_all_sent_to_clients_average_bytes – average between the number of bytes sent to clients by each document (active or expired).
+    document_all_sent_to_clients_min_bytes - minimum from the number of bytes sent to clients by each document (active or expired).
+    document_all_sent_to_clients_max_bytes - maximum from the number of bytes sent to clients by each document (active or expired).
+    document_active_sent_to_clients_total_bytes - total number of bytes sent to clients by active documents.
+    document_active_sent_to_clients_average_bytes - average between the number of bytes sent to clients by each active document.
+    document_active_sent_to_clients_min_bytes - minimum from the number of bytes sent to clients by each active document.
+    document_active_sent_to_clients_max_bytes - maximum from the number of bytes sent to clients by each active document.
+    document_expired_sent_to_clients_total_bytes - total number of bytes sent to clients by expired documents.
+    document_expired_sent_to_clients_average_bytes – average between the number of bytes sent to clients by each expired document.
+    document_expired_sent_to_clients_min_bytes - minimum from the number of bytes sent to clients by each expired document.
+    document_expired_sent_to_clients_max_bytes - maximum from the number of bytes sent to clients by each expired document.
+
+DOCUMENT BYTES RECEIVED FROM CLIENTS
+
+    document_all_received_from_clients_total_bytes - total number of bytes received from clients by all documents (active or expired).
+    document_all_received_from_clients_average_bytes – average between the number of bytes received from clients by each document (active or expired).
+    document_all_received_from_clients_min_bytes - minimum from the number of bytes received from clients by each document (active or expired).
+    document_all_received_from_clients_max_bytes - maximum from the number of bytes received from clients by each document (active or expired).
+    document_active_received_from_clients_total_bytes - total number of bytes received from clients by active documents.
+    document_active_received_from_clients_average_bytes - average between the number of bytes received from clients by each active document.
+    document_active_received_from_clients_min_bytes - minimum from the number of bytes received from clients by each active document.
+    document_active_received_from_clients_max_bytes - maximum from the number of bytes received from clients by each active document.
+    document_expired_received_from_clients_total_bytes - total number of bytes received from clients by expired documents.
+    document_expired_received_from_clients_average_bytes - average between the number of bytes received from clients by each expired document.
+    document_expired_received_from_clients_min_bytes - minimum from the number of bytes received from clients by each expired document.
+    document_expired_received_from_clients_max_bytes - maximum from the number of bytes received from clients by each expired document.
+
+DOCUMENT DOWNLOAD DURATION
+
+    document_all_wopi_download_duration_total_seconds - sum of download duration of each document (active or expired). 
+    document_all_wopi_download_duration_average_seconds – average between the download duration of each document (active or expired).
+    document_all_wopi_download_duration_min_seconds – minimum from the download duration of each document (active or expired).
+    document_all_wopi_download_duration_max_seconds - maximum from the download duration of each document (active or expired).
+    document_active_wopi_download_duration_total_seconds - sum of download duration of each active document.
+    document_active_wopi_download_duration_average_seconds - average between the download duration of each active document.
+    document_active_wopi_download_duration_min_seconds - minimum from the download duration of each active document.
+    document_active_wopi_download_duration_max_seconds - maximum from the download duration of each active document.
+    document_expired_wopi_download_duration_total_seconds - sum of download duration of each expired document.
+    document_expired_wopi_download_duration_average_seconds - average between the download duration of each expired document.
+    document_expired_wopi_download_duration_min_seconds - minimum from the download duration of each expired document.
+    document_expired_wopi_download_duration_max_seconds - maximum from the download duration of each expired document.
+
+DOCUMENT UPLOAD DURATION
+
+    document_all_wopi_upload_duration_total_seconds - sum of upload duration of each document (active or expired).
+    document_all_wopi_upload_duration_average_seconds – average between the upload duration of each document (active or expired).
+    document_all_wopi_upload_duration_min_seconds – minimum from the upload duration of each document (active or expired).
+    document_all_wopi_upload_duration_max_seconds - maximum from the upload duration of each document (active or expired).
+    document_active_wopi_upload_duration_total_seconds - sum of upload duration of each active document.
+    document_active_wopi_upload_duration_average_seconds - average between the upload duration of each active document.
+    document_active_wopi_upload_duration_min_seconds - minimum from the upload duration of each active document.
+    document_active_wopi_upload_duration_max_seconds - maximum from the upload duration of each active document.
+    document_expired_wopi_upload_duration_total_seconds - sum of upload duration of each expired document.
+    document_expired_wopi_upload_duration_average_seconds - average between the upload duration of each expired document.
+    document_expired_wopi_upload_duration_min_seconds - minimum from the upload duration of each expired document.
+    document_expired_wopi_upload_duration_max_seconds - maximum from the upload duration of each expired document.
+
+DOCUMENT VIEW LOAD DURATION
+
+    document_all_view_load_duration_total_seconds - sum of load duration of each view (active or expired) of each document (active or expired).
+    document_all_view_load_duration_average_seconds – average between the load duration of all views (active or expired) of each document (active or expired).
+    document_all_view_load_duration_min_seconds – minimum from the load duration of all views (active or expired) of each document (active or expired).
+    document_all_view_load_duration_max_seconds - maximum from the load duration of all views (active or expired) of each document (active or expired).
+    document_active_view_load_duration_total_seconds - sum of load duration of all views (active or expired) of each active document.
+    document_active_view_load_duration_average_seconds - average between the load duration of all views (active or expired) of each active document.
+    document_active_view_load_duration_min_seconds - minimum from the load duration of all views (active or expired) of each active document.
+    document_active_view_load_duration_max_seconds - maximum from the load duration of all views (active or expired) of each active document.
+    document_expired_view_load_duration_total_seconds - sum of load duration of all views (active or expired) of each expired document.
+    document_expired_view_load_duration_average_seconds - average between the load duration of all views (active or expired) of each expired document.
+    document_expired_view_load_duration_min_seconds - minimum from the load duration of all views (active or expired) of each expired document.
+    document_expired_view_load_duration_max_seconds - maximum from the load duration of all views (active or expired) of each expired document.
commit f92a4a7ba2413bd5b6249232fbd2a1843331f9d1
Author:     George <gwoodcode at gmail.com>
AuthorDate: Fri Jul 19 11:39:59 2019 +0100
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Mon Dec 16 16:54:07 2019 +0100

    added server uptime field to admin console
    
    Change-Id: Id23fee1299b87095f186ce7aaa8c2d2e0f3cef52
    Signed-off-by: Michael Meeks <michael.meeks at collabora.com>
    (cherry picked from commit 360d5a0956c39e696cc4fbab89ec6cdd2cf106d3)

diff --git a/loleaflet/admin/admin.html b/loleaflet/admin/admin.html
index ecd67cdb9..342d6ea80 100644
--- a/loleaflet/admin/admin.html
+++ b/loleaflet/admin/admin.html
@@ -85,6 +85,10 @@
 	      <div class="main-data" id="recv_bytes">0</div>
 	      <h4><script>document.write(l10nstrings.strRecvBytes)</script></h4>
 	    </div>
+	    <div class="col-xs-6 col-sm-2 placeholder">
+	      <div class="main-data" id="uptime">0</div>
+	      <h4><script>document.write(l10nstrings.strServerUptime)</script></h4>
+	    </div>
 	  </div>
     <div class="container-fluid">
       <ul class="nav nav-tabs">
diff --git a/loleaflet/admin/admin.strings.js b/loleaflet/admin/admin.strings.js
index 29713a8df..7dea1cdd3 100644
--- a/loleaflet/admin/admin.strings.js
+++ b/loleaflet/admin/admin.strings.js
@@ -40,6 +40,7 @@ l10nstrings.strDocuments = _('Documents:');
 l10nstrings.strExpired = _('Expired:');
 l10nstrings.strRefresh = _('Refresh');
 l10nstrings.strShutdown = _('Shutdown Server');
+l10nstrings.strServerUptime = _('Server uptime')
 
 if (module) {
 	module.exports = l10nstrings;
diff --git a/loleaflet/admin/src/AdminSocketOverview.js b/loleaflet/admin/src/AdminSocketOverview.js
index 15444e9a6..9c61089cc 100644
--- a/loleaflet/admin/src/AdminSocketOverview.js
+++ b/loleaflet/admin/src/AdminSocketOverview.js
@@ -51,6 +51,7 @@ var AdminSocketOverview = AdminSocketBase.extend({
 		this.socket.send('active_users_count');
 		this.socket.send('sent_bytes');
 		this.socket.send('recv_bytes');
+		this.socket.send('uptime')
 	},
 
 	onSocketOpen: function() {
@@ -311,7 +312,8 @@ var AdminSocketOverview = AdminSocketBase.extend({
 			textMsg.startsWith('active_docs_count') ||
 			textMsg.startsWith('active_users_count') ||
 			textMsg.startsWith('sent_bytes') ||
-			textMsg.startsWith('recv_bytes'))
+			textMsg.startsWith('recv_bytes') ||
+			textMsg.startsWith('uptime'))
 		{
 			textMsg = textMsg.split(' ');
 			var sCommand = textMsg[0];
@@ -322,6 +324,9 @@ var AdminSocketOverview = AdminSocketBase.extend({
 			    sCommand === 'recv_bytes') {
 				nData = Util.humanizeMem(nData);
 			}
+			else if (sCommand === 'uptime') {
+				nData = Util.humanizeSecs(nData)
+			}
 			$(document.getElementById(sCommand)).text(nData);
 		}
 		else if (textMsg.startsWith('rmdoc')) {
diff --git a/scripts/unocommands.py b/scripts/unocommands.py
index 3e5ec6bb8..a28c3ab5d 100755
--- a/scripts/unocommands.py
+++ b/scripts/unocommands.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python2
 # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
 #
 # This file is part of the LibreOffice project.
diff --git a/wsd/Admin.cpp b/wsd/Admin.cpp
index ea3d5b5be..9d9fc2548 100644
--- a/wsd/Admin.cpp
+++ b/wsd/Admin.cpp
@@ -152,6 +152,9 @@ void AdminSocketHandler::handleMessage(bool /* fin */, WSOpCode /* code */,
     else if (tokens[0] == "recv_bytes")
         sendTextFrame("recv_bytes " + std::to_string(model.getRecvBytesTotal() / 1024));
 
+    else if (tokens[0] == "uptime")
+        sendTextFrame("uptime " + std::to_string(model.getServerUptime()));
+
     else if (tokens[0] == "kill" && tokens.count() == 2)
     {
         try
diff --git a/wsd/AdminModel.cpp b/wsd/AdminModel.cpp
index df1529724..e0074ce5f 100644
--- a/wsd/AdminModel.cpp
+++ b/wsd/AdminModel.cpp
@@ -11,6 +11,7 @@
 
 #include "AdminModel.hpp"
 
+#include <chrono>
 #include <memory>
 #include <set>
 #include <sstream>
@@ -733,4 +734,11 @@ void AdminModel::updateMemoryDirty(const std::string& docKey, int dirty)
     }
 }
 
+double AdminModel::getServerUptime()
+{
+    auto currentTime = std::chrono::system_clock::now();
+    std::chrono::duration<double> uptime = currentTime - LOOLWSD::StartTime;
+    return uptime.count();
+}
+
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/AdminModel.hpp b/wsd/AdminModel.hpp
index 2b69f900b..57e7193e1 100644
--- a/wsd/AdminModel.hpp
+++ b/wsd/AdminModel.hpp
@@ -284,6 +284,8 @@ public:
     uint64_t getSentBytesTotal() { return _sentBytesTotal; }
     uint64_t getRecvBytesTotal() { return _recvBytesTotal; }
 
+    double getServerUptime();
+
     /// Document basic info list sorted by most idle time
     std::vector<DocBasicInfo> getDocumentsSortedByIdle() const;
 
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 0e0fe6098..f1e3d92fd 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -42,6 +42,7 @@
 #include <cstdlib>
 #include <cstring>
 #include <ctime>
+#include <chrono>
 #include <fstream>
 #include <iostream>
 #include <map>
@@ -716,6 +717,7 @@ unsigned LOOLWSD::MaxConnections;
 unsigned LOOLWSD::MaxDocuments;
 std::string LOOLWSD::OverrideWatermark;
 std::set<const Poco::Util::AbstractConfiguration*> LOOLWSD::PluginConfigurations;
+std::chrono::time_point<std::chrono::system_clock> LOOLWSD::StartTime;
 
 static std::string UnitTestLibrary;
 
@@ -774,6 +776,8 @@ void LOOLWSD::initialize(Application& self)
         throw std::runtime_error("Failed to load wsd unit test library.");
     }
 
+    StartTime = std::chrono::system_clock::now();
+
     auto& conf = config();
 
     // Add default values of new entries here.
diff --git a/wsd/LOOLWSD.hpp b/wsd/LOOLWSD.hpp
index 53fe4deb2..6928cb6f9 100644
--- a/wsd/LOOLWSD.hpp
+++ b/wsd/LOOLWSD.hpp
@@ -12,6 +12,7 @@
 
 #include <algorithm>
 #include <atomic>
+#include <chrono>
 #include <map>
 #include <set>
 #include <string>
@@ -73,6 +74,7 @@ public:
     static unsigned MaxDocuments;
     static std::string OverrideWatermark;
     static std::set<const Poco::Util::AbstractConfiguration*> PluginConfigurations;
+    static std::chrono::time_point<std::chrono::system_clock> StartTime;
 
     static std::vector<int> getKitPids();
 
commit e0522367c74ec144b82c0debd6ece302cbe823ec
Author:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
AuthorDate: Wed Oct 9 09:04:44 2019 +0200
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:50:49 2019 +0100

    One -f should be enough
    
    Change-Id: Iac8a6cb9fe7a6da41c3d7120bafa62e5847f2c98
    Reviewed-on: https://gerrit.libreoffice.org/80510
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>
    (cherry picked from commit 05831c52f795ab69bcf7fef5f7b359b2d95c95ee)

diff --git a/docker/l10n-docker-nightly.sh b/docker/l10n-docker-nightly.sh
index 9c8b39416..9f1f35a57 100755
--- a/docker/l10n-docker-nightly.sh
+++ b/docker/l10n-docker-nightly.sh
@@ -65,7 +65,7 @@ if test ! -d online ; then
     git clone https://git.libreoffice.org/online online || exit 1
 fi
 
-( cd online && git fetch --all && git checkout -f $LIBREOFFICE_BRANCH && git clean -f -f -d && git pull -r ) || exit 1
+( cd online && git fetch --all && git checkout -f $LIBREOFFICE_BRANCH && git clean -f -d && git pull -r ) || exit 1
 
 ##### LibreOffice #####
 
commit 16195172c7b9110c21fdb8352aa622ac1ca952dd
Author:     Andras Timar <andras.timar at collabora.com>
AuthorDate: Wed Oct 9 08:54:24 2019 +0200
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:50:35 2019 +0100

    docker: run git clean before git pull
    
    Fixes:
    https://ci.libreoffice.org/job/lo_online_docker_master/118/console
    
    Change-Id: Ib7f228cdbb191f0a397849fb6adf20b37591c1c1
    (cherry picked from commit 340b2ad8d492635387d6094e3915cb349aa99d39)

diff --git a/docker/l10n-docker-nightly.sh b/docker/l10n-docker-nightly.sh
index bf0c31d5e..9c8b39416 100755
--- a/docker/l10n-docker-nightly.sh
+++ b/docker/l10n-docker-nightly.sh
@@ -65,7 +65,7 @@ if test ! -d online ; then
     git clone https://git.libreoffice.org/online online || exit 1
 fi
 
-( cd online && git fetch --all && git checkout -f $LIBREOFFICE_BRANCH && git pull -r ) || exit 1
+( cd online && git fetch --all && git checkout -f $LIBREOFFICE_BRANCH && git clean -f -f -d && git pull -r ) || exit 1
 
 ##### LibreOffice #####
 
commit 2792f8c9735829cd99469c110dddfb4d83e673d7
Author:     Szymon Kłos <szymon.klos at collabora.com>
AuthorDate: Thu Jul 25 13:52:50 2019 +0200
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:44:56 2019 +0100

    Don't show join notification for only user
    
    Change-Id: Ibc3998cb9b760a4050ddecff602b494bf515cdf8
    Reviewed-on: https://gerrit.libreoffice.org/79938
    Reviewed-by: Szymon Kłos <szymon.klos at collabora.com>
    Tested-by: Szymon Kłos <szymon.klos at collabora.com>
    (cherry picked from commit 8c15584b4aff126bcde01733f469ac42cf0ce3e8)

diff --git a/loleaflet/src/control/Control.Toolbar.js b/loleaflet/src/control/Control.Toolbar.js
index ac4636249..1aa57aaa4 100644
--- a/loleaflet/src/control/Control.Toolbar.js
+++ b/loleaflet/src/control/Control.Toolbar.js
@@ -2222,19 +2222,27 @@ function escapeHtml(input) {
 }
 
 function onAddView(e) {
+	var userlistItem = w2ui['actionbar'].get('userlist');
 	var username = escapeHtml(e.username);
-	$('#tb_actionbar_item_userlist')
-		.w2overlay({
-			class: 'loleaflet-font',
-			html: userJoinedPopupMessage.replace('%user', username),
-			style: 'padding: 5px'
-		});
-	clearTimeout(userPopupTimeout);
-	userPopupTimeout = setTimeout(function() {
-		$('#tb_actionbar_item_userlist').w2overlay('');
+	var showPopup = false;
+
+	if (userlistItem !== null)
+		showPopup = $(userlistItem.html).find('#userlist_table tbody tr').length > 0;
+
+	if (showPopup) {
+		$('#tb_actionbar_item_userlist')
+			.w2overlay({
+				class: 'loleaflet-font',
+				html: userJoinedPopupMessage.replace('%user', username),
+				style: 'padding: 5px'
+			});
 		clearTimeout(userPopupTimeout);
-		userPopupTimeout = null;
-	}, 3000);
+		userPopupTimeout = setTimeout(function() {
+			$('#tb_actionbar_item_userlist').w2overlay('');
+			clearTimeout(userPopupTimeout);
+			userPopupTimeout = null;
+		}, 3000);
+	}
 
 	var color = L.LOUtil.rgbToHex(map.getViewColor(e.viewId));
 	if (e.viewId === map._docLayer._viewId) {
@@ -2247,7 +2255,6 @@ function onAddView(e) {
 		username += ' (' +  _('Readonly') + ')';
 	}
 
-	var userlistItem = w2ui['actionbar'].get('userlist');
 	if (userlistItem !== null) {
 		var newhtml = $(userlistItem.html).find('#userlist_table tbody').append(getUserItem(e.viewId, username, e.extraInfo, color)).parent().parent()[0].outerHTML;
 		userlistItem.html = newhtml;
commit 88c86ef37a00d3f67e37ce7a4c1b8712451bfac5
Author:     Szymon Kłos <szymon.klos at collabora.com>
AuthorDate: Wed Jul 24 13:37:22 2019 +0200
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:44:49 2019 +0100

    Fix language selection in menubar
    
    Change-Id: I990105361decf47f0794853a5588e9f3f66e9e6e
    Reviewed-on: https://gerrit.libreoffice.org/79937
    Reviewed-by: Szymon Kłos <szymon.klos at collabora.com>
    Tested-by: Szymon Kłos <szymon.klos at collabora.com>
    (cherry picked from commit 8ddb191d5d778962575702ff326b543f187c4ca1)

diff --git a/loleaflet/src/control/Control.Menubar.js b/loleaflet/src/control/Control.Menubar.js
index 93e91b714..5503b26fe 100644
--- a/loleaflet/src/control/Control.Menubar.js
+++ b/loleaflet/src/control/Control.Menubar.js
@@ -650,7 +650,7 @@ L.Control.Menubar = L.Control.extend({
 			var constChecked = 'lo-menu-item-checked';
 			if (self._map._permission === 'edit') {
 				if (type === 'unocommand') { // enable all depending on stored commandStates
-					var data, lang;
+					var data, lang, languageAndCode;
 					var constUno = 'uno';
 					var constState = 'stateChangeHandler';
 					var constLanguage = '.uno:LanguageStatus';
@@ -665,7 +665,8 @@ L.Control.Menubar = L.Control.extend({
 					}
 					if (unoCommand.startsWith(constLanguage)) {
 						unoCommand = constLanguage;
-						lang = self._map[constState].getItemValue(unoCommand);
+						languageAndCode = self._map[constState].getItemValue(unoCommand);
+						lang = languageAndCode.split(';')[0];
 						data = decodeURIComponent($(aItem).data(constUno));
 						if (data.indexOf(lang) !== -1) {
 							$(aItem).addClass(constChecked);
commit 3b1087906787ad7824595bfaf492dcac812672d7
Author:     Jan Holesovsky <kendy at collabora.com>
AuthorDate: Mon Sep 23 13:06:52 2019 +0200
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:43:31 2019 +0100

    tdf#127663: Don't emit 'uno .uno:FontHeight ...' multiple times.
    
    Otherwise it will end up several times on the undo stack, and the user
    has to hit undo several times before undone.
    
    Change-Id: I089c9c9a521e1315b1f85b2866d04ee23a10d3b1
    Reviewed-on: https://gerrit.libreoffice.org/79401
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>
    (cherry picked from commit 311e93fb90cd9480368eeabf2e5adc264af30eed)

diff --git a/loleaflet/src/control/Control.Toolbar.js b/loleaflet/src/control/Control.Toolbar.js
index 9e77cb149..ac4636249 100644
--- a/loleaflet/src/control/Control.Toolbar.js
+++ b/loleaflet/src/control/Control.Toolbar.js
@@ -1537,7 +1537,7 @@ function onDocLayerInit() {
 			return parseFloat(a.text) - parseFloat(b.text);
 		})}
 	});
-	$('.fontsizes-select').on('select2:select', onFontSizeSelect);
+	$('.fontsizes-select').off('select2:select', onFontSizeSelect).on('select2:select', onFontSizeSelect);
 }
 
 function onCommandStateChanged(e) {
commit 113ae8ad044a57271658ba2e491027a67a63f11e
Author:     Andras Timar <andras.timar at collabora.com>
AuthorDate: Thu Sep 5 13:15:02 2019 +0200
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:41:29 2019 +0100

    docker: install selective set of poco libraries, only what we need
    
    Change-Id: I2776b6817696fbc8d5f24a35165e26bf5ff8c4af
    Reviewed-on: https://gerrit.libreoffice.org/78646
    Reviewed-by: Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>
    (cherry picked from commit d5cf8afd5762453235f497145e26a5978cc66e2c)

diff --git a/docker/Dockerfile b/docker/Dockerfile
index d8109f1ea..4b352321b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -12,7 +12,7 @@ RUN apt-get update && apt-get upgrade -y
 # install LibreOffice run-time dependencies
 # install adduser, findutils, openssl and cpio that we need later
 # install an editor
-RUN apt-get -y install locales-all libpng16-16 libxinerama1 libgl1-mesa-glx libfontconfig1 libfreetype6 libxrender1 libxcb-shm0 libxcb-render0 adduser cpio findutils nano libpoco*50 libcap2-bin openssl inotify-tools procps
+RUN apt-get -y install locales-all libpng16-16 libxinerama1 libgl1-mesa-glx libfontconfig1 libfreetype6 libxrender1 libxcb-shm0 libxcb-render0 adduser cpio findutils nano libpocoxml50 libpocoutil50 libpoconetssl50 libpoconet50 libpocojson50 libpocofoundation50 libpococrypto50 libcap2-bin openssl inotify-tools procps
 
 # tdf#117557 - Add CJK Fonts to LibreOffice Online Docker Image
 RUN apt-get -y install fonts-wqy-zenhei fonts-wqy-microhei fonts-droid-fallback fonts-noto-cjk
commit aa97483c8c20805c53a08f265b42e3ced89d6bba
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Wed Aug 28 21:18:53 2019 -0400
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:41:12 2019 +0100

    wsd: sanitize url when error reporting
    
    (cherry picked from commit 2b35ae713943abd5f51de383fb2d26ab96f73988)
    
    Change-Id: I7937429f2f987212beaeb9a97b48bfedb0a7ac58
    (cherry picked from commit 97a7b1f170e14c3fbbaed795ba8591a10831fa4e)

diff --git a/wsd/FileServer.cpp b/wsd/FileServer.cpp
index cbdcea172..e91f3be73 100644
--- a/wsd/FileServer.cpp
+++ b/wsd/FileServer.cpp
@@ -445,8 +445,10 @@ void FileServerRequestHandler::sendError(int errorCode, const Poco::Net::HTTPReq
         << "\r\n";
     if (!shortMessage.empty())
     {
+        std::string pathSanitized;
+        Poco::URI::encode(path, "", pathSanitized);
         oss << "<h1>Error: " << shortMessage << "</h1>"
-            "<p>" << longMessage << " " << path << "</p>"
+            "<p>" << longMessage << ' ' << pathSanitized << "</p>"
             "<p>Please contact your system administrator.</p>";
     }
     socket->send(oss.str());
@@ -723,6 +725,7 @@ void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::
         LOG_TRC("Denied all frame ancestors");
         cspOss << "img-src 'self' data: none;";
     }
+
     cspOss << "\r\n";
     // Append CSP to response headers too
     oss << cspOss.str();
commit 480ad39ae6ad26107c3bbb33bb8ab81c698d784e
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sat Jun 29 10:52:11 2019 -0400
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:40:47 2019 +0100

    leaflet: fire Doc_ModifiedStatus with modified state of the document
    
    When .uno:ModifiedStatus is received, now Doc_ModifiedStatus
    is fired to inform the client of the modified state of the document.
    
    This is useful in case the integration needs to prompt the user to save
    before closing the document (which they can catch with the onunload or
    onbeforeunload events in the browser, as well as with our
    UI_Close when the default handler is disabled).
    
    Includes working sample and documentation.
    
    Change-Id: Ief30483e2f078b0aa9f3c006a1ecb4093375174c
    Reviewed-on: https://gerrit.libreoffice.org/74891
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>
    Tested-by: Jan Holesovsky <kendy at collabora.com>
    (cherry picked from commit 8f312862ca1e3d472820b70c852ff5c7afa62ddc)

diff --git a/loleaflet/html/framed.doc.html b/loleaflet/html/framed.doc.html
index d89a1a98f..80f842c40 100644
--- a/loleaflet/html/framed.doc.html
+++ b/loleaflet/html/framed.doc.html
@@ -87,12 +87,30 @@
       function receiveMessage(event) {
         console.log('==== framed.doc.html receiveMessage: ' + event.data);
         var msg = JSON.parse(event.data);
-        if (msg && msg.MessageId == 'App_LoadingStatus') {
+        if (!msg) {
+          return;
+        }
+        if (msg.MessageId == 'App_LoadingStatus') {
           if (msg.Values) {
             if (msg.Values.Status == 'Document_Loaded') {
               window.frames[0].postMessage(JSON.stringify({'MessageId': 'Host_PostmessageReady'}), '*');
             }
           }
+        } else if (msg.MessageId == 'Doc_ModifiedStatus') {
+          if (msg.Values) {
+            if (msg.Values.Modified == true) {
+              document.getElementById("ModifiedStatus").innerHTML = "Modified";
+            }
+            else {
+              document.getElementById("ModifiedStatus").innerHTML = "Saved";
+            }
+          }
+        } else if (msg.MessageId == 'Action_Save_Resp') {
+          if (msg.Values) {
+            if (msg.Values.success == true) {
+              document.getElementById("ModifiedStatus").innerHTML = "Saved";
+            }
+          }
         }
       }
 
@@ -125,6 +143,10 @@
       <button onclick="disable_default_uiaction('UI_Save', false); return false;">Enable default save action</button></br></br>
     </form>
 
+    <p>Modified Status:
+    <span id="ModifiedStatus">Saved</span>
+    </p>
+
     <!-- The hostname and pathnames below are obviously specific to my
     personal environment and need to be changed appropriately. Also
     the hex string needs to be changed of course, to the right one as
diff --git a/loleaflet/reference.html b/loleaflet/reference.html
index 492908bfa..6010cf3a3 100644
--- a/loleaflet/reference.html
+++ b/loleaflet/reference.html
@@ -3377,6 +3377,21 @@ Note that they usually don't change but there is no guarantee that they are stab
 		Response to this query is sent via <code>Action_SaveAs</code> message.
 		</td>
 	</tr>
+	<tr>
+		<td><code><b>Doc_ModifiedStatus</b></code></td>
+		<td></td>
+		<td>
+		Notification to update the modified status of the document.
+		Values.Modified will be true, if the document has been modified
+		since the last save, otherwise, it will be false if the document
+		has been saved.
+
+		Note that this notification may be published without a change
+		from the prior value, so care must be taken to check the Values.Modified
+		value and not assume the notifiaction itself implies the
+		modified state of the document on its own.
+		</td>
+	</tr>
 </table>
 
 <h3 id='loleaflet-postmessage-python'>Calling Python scripts</h3>
diff --git a/loleaflet/src/map/Map.js b/loleaflet/src/map/Map.js
index fcee4166d..360bfa019 100644
--- a/loleaflet/src/map/Map.js
+++ b/loleaflet/src/map/Map.js
@@ -215,8 +215,12 @@ L.Map = L.Evented.extend({
 		this._docLoaded = false;
 
 		this.on('commandstatechanged', function(e) {
-			if (e.commandName === '.uno:ModifiedStatus')
+			if (e.commandName === '.uno:ModifiedStatus') {
 				this._everModified = this._everModified || (e.state === 'true');
+
+				// Fire an event to let the client know whether the document needs saving or not.
+				this.fire('postMessage', {msgId: 'Doc_ModifiedStatus', args: { Modified: e.state === 'true' }});
+			}
 		}, this);
 
 		this.on('docloaded', function(e) {
diff --git a/test/data/empty.odt b/test/data/empty.odt
index 208d2f874..6b0747507 100644
Binary files a/test/data/empty.odt and b/test/data/empty.odt differ
commit c275ce971d4ab576fb4f0248181c384bc6a2a3d2
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sun Jun 16 14:42:11 2019 -0400
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:40:41 2019 +0100

    Pass Extended Data in Action_Save from the client to the WOPI host
    
    Clients often need to communicate with their wopi hosts when
    invoking Action_Save to provide more context when storing the
    document in question. Action_Save now support passing arbitrary
    string as ExtendedData entry that can be used by client to
    pass any context or otherwise flags to the WOPI host, which
    will receive it via the X-LOOL-WOPI-ExtendedData custom header.
    
    See reference.html for more details.
    
    Change-Id: I1814d1f3d984a553ffa60cec13d23b014ba59eb3
    Reviewed-on: https://gerrit.libreoffice.org/74135
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>
    (cherry picked from commit 3dcc68e6f12a1b0d2e1c586a6d4c8ccdfeb8a15b)

diff --git a/loleaflet/html/framed.doc.html b/loleaflet/html/framed.doc.html
index bd62fddeb..d89a1a98f 100644
--- a/loleaflet/html/framed.doc.html
+++ b/loleaflet/html/framed.doc.html
@@ -48,7 +48,7 @@
 
       function save() {
         post({'MessageId': 'Action_Save',
-              'Values': { 'Notify': true, }
+              'Values': { 'Notify': true, 'ExtendedData': 'CustomFlag=CustomValue;AnotherFlag=AnotherValue' }
             });
       }
 
diff --git a/loleaflet/reference.html b/loleaflet/reference.html
index 0441513ca..492908bfa 100644
--- a/loleaflet/reference.html
+++ b/loleaflet/reference.html
@@ -2938,6 +2938,7 @@ Editor to WOPI host
 		    <nobr>DontTerminateEdit: <boolean></nobr>
 		    <nobr>DontSaveIfUnmodified: <boolean></nobr>
 		    <nobr>Notify: <boolean></nobr>
+		    <nobr>ExtendedData: <String></nobr>
 		</code></td>
 		<td>Saves the document.<br/>
 		<code>DontTerminateEdit</code> is relevant for spreadsheets where saving
@@ -2946,10 +2947,16 @@ Editor to WOPI host
 		user's editing session in spreadsheets.<br/>
 		<code>DontSaveIfUnmodified</code> prevents loolwsd to save the file back to storage if document is
 		unmodified (only cursor position changed etc.) but still saved. This can be helpful
-		to prevent creating unnecessary file revisions.
+		to prevent creating unnecessary file revisions.<br/>
 		<code>Notify</code> when present and set to true notifies the
 		host when document is saved. See <code>Action_Save_Resp</code>
-		for details.
+		for details.<br/>
+		<code>ExtendedData</code> optional data carried over to the WOPI host if provided
+		in the X-LOOL-WOPI-ExtendedData header. The contents are preserved as-is,
+		however, care must be taken to avoid using anything that HTTP headers do
+		not allow, also, special values such as new-line, null character, non-printable
+		characters, etc. are not allowed. The client can use this to pass multiple values
+		to the WOPI host which can then act on them.<br/>
 		</td>
 	</tr>
 	<tr>
diff --git a/loleaflet/src/control/Toolbar.js b/loleaflet/src/control/Toolbar.js
index f75c0ae31..6cc3ba875 100644
--- a/loleaflet/src/control/Toolbar.js
+++ b/loleaflet/src/control/Toolbar.js
@@ -143,10 +143,16 @@ L.Map.include({
 		}
 	},
 
-	save: function(dontTerminateEdit, dontSaveIfUnmodified) {
-		this._socket.sendMessage('save' +
-		                         ' dontTerminateEdit=' + (dontTerminateEdit ? 1 : 0) +
-		                         ' dontSaveIfUnmodified=' + (dontSaveIfUnmodified ? 1 : 0));
+	save: function(dontTerminateEdit, dontSaveIfUnmodified, extendedData) {
+		var msg = 'save' +
+					' dontTerminateEdit=' + (dontTerminateEdit ? 1 : 0) +
+					' dontSaveIfUnmodified=' + (dontSaveIfUnmodified ? 1 : 0);
+
+		if (extendedData !== undefined) {
+			msg += ' extendedData=' + extendedData;
+		}
+
+		this._socket.sendMessage(msg);
 	},
 
 	sendUnoCommand: function (command, json) {
diff --git a/loleaflet/src/map/handler/Map.WOPI.js b/loleaflet/src/map/handler/Map.WOPI.js
index 775791bbd..c2627c9bb 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -318,9 +318,10 @@ L.Map.WOPI = L.Handler.extend({
 		else if (msg.MessageId === 'Action_Save') {
 			var dontTerminateEdit = msg.Values && msg.Values['DontTerminateEdit'];
 			var dontSaveIfUnmodified = msg.Values && msg.Values['DontSaveIfUnmodified'];
+			var extendedData = msg.Values && msg.Values['ExtendedData'];
 			this._notifySave = msg.Values && msg.Values['Notify'];
 
-			this._map.save(dontTerminateEdit, dontSaveIfUnmodified);
+			this._map.save(dontTerminateEdit, dontSaveIfUnmodified, extendedData);
 		}
 		else if (msg.MessageId === 'Action_Close') {
 			this._map.remove();
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 19634a49d..8ac41527e 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -273,7 +273,14 @@ bool ClientSession::_handleInput(const char *buffer, int length)
             if (tokens.size() > 2)
                 getTokenInteger(tokens[2], "dontSaveIfUnmodified", dontSaveIfUnmodified);
 
-            docBroker->sendUnoSave(getId(), dontTerminateEdit != 0, dontSaveIfUnmodified != 0);
+            std::string extendedData;
+            if (tokens.size() > 3)
+                getTokenString(tokens[3], "extendedData", extendedData);
+
+            constexpr bool isAutosave = false;
+            constexpr bool isExitSave = false;
+            docBroker->sendUnoSave(getId(), dontTerminateEdit != 0, dontSaveIfUnmodified != 0,
+                                    isAutosave, isExitSave, extendedData);
         }
     }
     else if (tokens[0] == "savetostorage")
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index f6327b6e7..9431e0fef 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -1068,7 +1068,8 @@ bool DocumentBroker::autoSave(const bool force)
 }
 
 bool DocumentBroker::sendUnoSave(const std::string& sessionId, bool dontTerminateEdit,
-                                 bool dontSaveIfUnmodified, bool isAutosave, bool isExitSave)
+                                 bool dontSaveIfUnmodified, bool isAutosave, bool isExitSave,
+                                 const std::string& extendedData)
 {
     assertCorrectThread();
 
@@ -1112,6 +1113,7 @@ bool DocumentBroker::sendUnoSave(const std::string& sessionId, bool dontTerminat
         assert(_storage);
         _storage->setIsAutosave(isAutosave || UnitWSD::get().isAutosave());
         _storage->setIsExitSave(isExitSave);
+        _storage->setExtendedData(extendedData);
 
         const std::string saveArgs = oss.str();
         LOG_TRC(".uno:Save arguments: " << saveArgs);
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 1923d3b93..a31727d7a 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -348,7 +348,7 @@ public:
     /// Sends the .uno:Save command to LoKit.
     bool sendUnoSave(const std::string& sessionId, bool dontTerminateEdit = true,
                      bool dontSaveIfUnmodified = true, bool isAutosave = false,
-                     bool isExitSave = false);
+                     bool isExitSave = false, const std::string& extendedData = std::string());
 
     /// Sends a message to all sessions
     void broadcastMessage(const std::string& message);
diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp
index d29bbaa63..dd26d7b38 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -729,6 +729,8 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
             request.set("X-LOOL-WOPI-IsModifiedByUser", isUserModified()? "true": "false");
             request.set("X-LOOL-WOPI-IsAutosave", getIsAutosave()? "true": "false");
             request.set("X-LOOL-WOPI-IsExitSave", isExitSave()? "true": "false");
+            if (!getExtendedData().empty())
+                request.set("X-LOOL-WOPI-ExtendedData", getExtendedData());
 
             if (!getForceSave())
             {
diff --git a/wsd/Storage.hpp b/wsd/Storage.hpp
index 02539b8f1..ed1c69a69 100644
--- a/wsd/Storage.hpp
+++ b/wsd/Storage.hpp
@@ -179,6 +179,7 @@ public:
     bool getIsAutosave() const { return _isAutosave; }
     void setIsExitSave(bool exitSave) { _isExitSave = exitSave; }
     bool isExitSave() const { return _isExitSave; }
+    void setExtendedData(const std::string& extendedData) { _extendedData = extendedData; }
 
     void setFileInfo(const FileInfo& fileInfo) { _fileInfo = fileInfo; }
 
@@ -211,6 +212,9 @@ protected:
     /// Returns the root path of the jail directory of docs.
     std::string getLocalRootPath() const;
 
+    /// Returns the client-provided extended data to send to the WOPI host.
+    const std::string& getExtendedData() const { return _extendedData; }
+
 private:
     const Poco::URI _uri;
     std::string _localStorePath;
@@ -228,6 +232,8 @@ private:
     bool _isAutosave;
     /// Saving on exit (when the document is cleaned up from memory)
     bool _isExitSave;
+    /// The client-provided saving extended data to send to the WOPI host.
+    std::string _extendedData;
 
     static bool FilesystemEnabled;
     static bool WopiEnabled;
commit c366b1753a2976baf16ed5efa95c94c2397f8498
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sun Jun 16 14:16:06 2019 -0400
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:40:30 2019 +0100

    leaflet: Support notifying the client on Action_SaveAs completion
    
    Clients expect a consistent notification API for Action_Save
    and Action_SaveAs. Unfortunately, Action_SaveAs didn't
    support notifying the client as Action_Save does.
    
    Now, when Notify is set to true in the Action_SaveAs
    message, the client will be notified with Action_Save_Resp.
    
    Change-Id: Ib8eb946c3bc642cfd46124e1190e931c21f88de0
    Reviewed-on: https://gerrit.libreoffice.org/74134
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>
    (cherry picked from commit 5ff0a42b94bc06723a2e592b649c28717c88512f)

diff --git a/loleaflet/reference.html b/loleaflet/reference.html
index 54d59f12c..0441513ca 100644
--- a/loleaflet/reference.html
+++ b/loleaflet/reference.html
@@ -2956,9 +2956,13 @@ Editor to WOPI host
 		<td><code><b>Action_SaveAs</b></code></td>
 		<td><code>
 		    <nobr>Filename: <String></nobr>
+		    <nobr>Notify: <boolean></nobr>
 		</code></td>
 		<td>Creates copy of the document with given Filename.<br/>
 		<code>Filename</code> is the requested filename for the new file.<br/>
+		<code>Notify</code> when present and set to true notifies the
+		host when document is saved. See <code>Action_Save_Resp</code>
+		for details.
 		</td>
 	</tr>
 	<tr>
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index 366918123..6432414ed 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -672,6 +672,12 @@ L.Socket = L.Class.extend({
 						}
 					});
 				}
+
+				// Issue the save response to be consistent with normal save.
+				var postMessageObj = {
+					success: true
+				};
+				this._map.fire('postMessage', {msgId: 'Action_Save_Resp', args: postMessageObj});
 			}
 			// var name = command.name; - ignored, we get the new name via the wopi's BaseFileName
 		}
diff --git a/loleaflet/src/map/handler/Map.WOPI.js b/loleaflet/src/map/handler/Map.WOPI.js
index 608c741bd..775791bbd 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -364,6 +364,7 @@ L.Map.WOPI = L.Handler.extend({
 		else if (msg.MessageId === 'Action_SaveAs') {
 			if (msg.Values) {
 				if (msg.Values.Filename !== null && msg.Values.Filename !== undefined) {
+					this._notifySave = msg.Values['Notify'];
 					this._map.showBusy(_('Creating copy...'), false);
 					this._map.saveAs(msg.Values.Filename);
 				}
commit 1aa592954e48b5f34ee13859674aa4df384de02c
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sun Jun 16 13:28:58 2019 -0400
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:40:11 2019 +0100

    Cleanup framed.doc.html sample
    
    Now Host_PostmessageReady is automatically issued
    upon loading and the postMessage calls are more modular,
    allowing for expansion with more functionality.
    
    Change-Id: I22b50f7228e0fd32c4cb880f4981c1a455038d48
    Reviewed-on: https://gerrit.libreoffice.org/74129
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>
    (cherry picked from commit 4dc312f5492c75d4afdc6e333dd4d85079e84dfc)

diff --git a/loleaflet/html/framed.doc.html b/loleaflet/html/framed.doc.html
index 1dfe7f27b..bd62fddeb 100644
--- a/loleaflet/html/framed.doc.html
+++ b/loleaflet/html/framed.doc.html
@@ -10,9 +10,9 @@
 
      To test this, do 'make run', and then in your browser open the
      equivalent of
-     http://snorken.local:9980/loleaflet/3304e9093/framed.html if the
+     http://snorken.local:9980/loleaflet/3304e9093/framed.doc.html if the
      browser is running on a different machine, or
-     http://localhost:9980/loleaflet/3304e9093/framed.html if running
+     http://localhost:9980/loleaflet/3304e9093/framed.doc.html if running
      on the same machine.
 
 -->
@@ -24,26 +24,26 @@
 
     <script>
 
+      function post(msg) {
+        window.frames[0].postMessage(JSON.stringify(msg), '*');
+      }
+
       function insertText(text) {
-        window.frames[0].postMessage(JSON.stringify({'MessageId': 'Host_PostmessageReady'}), '*');
-        window.frames[0].postMessage(JSON.stringify({'MessageId': 'CallPythonScript',
-                                                     'SendTime': Date.now(),
-                                                     'ScriptFile': 'InsertText.py',
-                                                     'Function': 'InsertText',
-                                                     'Values': { 'text': {'type': 'string', 'value': text}}
-                                                     }),
-                                     '*');
+        post({'MessageId': 'CallPythonScript',
+              'SendTime': Date.now(),
+              'ScriptFile': 'InsertText.py',
+              'Function': 'InsertText',
+              'Values': { 'text': {'type': 'string', 'value': text}}
+            });
       }
 
       function capitalize() {
-        window.frames[0].postMessage(JSON.stringify({'MessageId': 'Host_PostmessageReady'}), '*');
-        window.frames[0].postMessage(JSON.stringify({'MessageId': 'CallPythonScript',
-                                                     'SendTime': Date.now(),
-                                                     'ScriptFile': 'Capitalise.py',
-                                                     'Function': 'capitalisePython',
-                                                     'Values': null
-                                                     }),
-                                     '*');
+        post({'MessageId': 'CallPythonScript',
+              'SendTime': Date.now(),
+              'ScriptFile': 'Capitalise.py',
+              'Function': 'capitalisePython',
+              'Values': null
+            });
       }
 
       function save() {
@@ -85,9 +85,15 @@
       // This function is invoked when the iframe posts a message back.
 
       function receiveMessage(event) {
+        console.log('==== framed.doc.html receiveMessage: ' + event.data);
         var msg = JSON.parse(event.data);
-        console.log('==== framed.html receiveMessage: ' + event.data);
-        console.log('                                 ' + msg);
+        if (msg && msg.MessageId == 'App_LoadingStatus') {
+          if (msg.Values) {
+            if (msg.Values.Status == 'Document_Loaded') {
+              window.frames[0].postMessage(JSON.stringify({'MessageId': 'Host_PostmessageReady'}), '*');
+            }
+          }
+        }
       }
 
       // 'main' code of this <script> block, run when page is being
commit 7db2ffa85c728570a23313c515d91b264fa92158
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sun Jun 16 13:16:20 2019 -0400
Commit:     Samuel Mehrbrodt <Samuel.Mehrbrodt at cib.de>
CommitDate: Thu Dec 12 18:39:32 2019 +0100

    leaflet: support disabling default action
    
    Clients often need to handle certain commands themselves.
    This is especially true for Action_Save and Action_Close.
    
    A new postMessage command, Disable_Default_UIAction, is
    now available to support disabling/enabling the default
    action for certain commands (as of this patch, only
    Action_Save and Action_Close are supported).
    
    The actions in question issue a notification and,
    when the default handler is disabled, the client
    is expected to handle the notification for which
    they disabled the default handler and act as necessary.
    
    See reference.html for more details.
    
    Change-Id: Ia6ce4e2d7578f79cc2069097e0b968e6c4aeabd1
    Reviewed-on: https://gerrit.libreoffice.org/74136
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>
    (cherry picked from commit df6cc01c325f215ba90afe7665365d269c582fd1)

diff --git a/loleaflet/html/framed.doc.html b/loleaflet/html/framed.doc.html
index 5501d5df6..1dfe7f27b 100644
--- a/loleaflet/html/framed.doc.html
+++ b/loleaflet/html/framed.doc.html
@@ -76,6 +76,12 @@
             });
       }
 
+      function disable_default_uiaction(action, disable) {
+        post({'MessageId': 'Disable_Default_UIAction',
+              'Values': { 'action': action, 'disable': disable }
+            });
+      }
+
       // This function is invoked when the iframe posts a message back.
 
       function receiveMessage(event) {
@@ -109,6 +115,8 @@
       <button onclick="show_commands('save'); return false;">Show Save Commands</button></br>
       <button onclick="hide_commands('print'); return false;">Hide Print Commands</button>
       <button onclick="show_commands('print'); return false;">Show Print Commands</button></br></br>
+      <button onclick="disable_default_uiaction('UI_Save', true); return false;">Disable default save action</button></br>
+      <button onclick="disable_default_uiaction('UI_Save', false); return false;">Enable default save action</button></br></br>
     </form>
 
     <!-- The hostname and pathnames below are obviously specific to my
diff --git a/loleaflet/reference.html b/loleaflet/reference.html
index 0acd2d726..54d59f12c 100644
--- a/loleaflet/reference.html
+++ b/loleaflet/reference.html
@@ -3277,6 +3277,47 @@ Editor to WOPI host
 			<a href="https://opengrok.libreoffice.org/xref/online/loleaflet/src/control/Control.Menubar.js">loleaflet/src/control/Control.Menubar.js</a>.
 		</td>
 	</tr>
+	<tr>
+		<td><code><b>Disable_Default_UIAction</b></code></td>
+		<td>
+			<code><nobr>action: <string></nobr></code>
+			<code><nobr>disable: <Boolean></nobr></code>
+		</td>
+		<td>
+			Disable the default handler and action for a UI command.<br/>
+
+			<code>action</code> is the action name to enable/disable the
+			default action for.<br/>
+
+			<code>disable</code> controls whether to disable (true) or
+			enable (false) the default action.<br/>
+
+			When set to true, the given UI command will only issue a postMessage
+			without invoking the default action, leaving it up to the client
+			to intercept the postMessage event and handle as necessary.
+			Notice that some actions do not have any default handler to
+			begin with (such as UI_SaveAs and UI_Share) and therefore this
+			will have no effect on them; they only issue postMessage notification
+			anyway without taking any action beyond that.<br/>
+
+			For example, UI_Save will be issued for invoking the save
+			command (from the menu, toolbar, or keyboard shortcut) and no
+			action will take place if 'UI_Save' is disabled via
+			the Disable_Default_UIAction command. Clients who disable
+			UI_Save should then issue Action_Save themselves, when and
+			if they desire to save the document.
+			Similarly, when disabling UI_Close, the document will not
+			close upon invoking the UI_Close action, instead a postMessage
+			notification will be issued and it will be up to the client
+			to issue Action_Close when they desire.<br/>
+
+			Clients must be careful not to issue duplicate actions when
+			the default handler is enabled, instead, they should only
+			issue actions themselves when the default is disabled.
+
+			Note: currently only UI_Save and UI_Close are supported.<br/>
+		</td>
+	</tr>
 </table>
 
 <h5><a name="toolbar-button-ids">Finding toolbar button IDs</a></h5>
diff --git a/loleaflet/src/control/Control.Menubar.js b/loleaflet/src/control/Control.Menubar.js
index 5b45305aa..93e91b714 100644
--- a/loleaflet/src/control/Control.Menubar.js
+++ b/loleaflet/src/control/Control.Menubar.js
@@ -743,7 +743,10 @@ L.Control.Menubar = L.Control.extend({
 	_executeAction: function(item) {
 		var id = $(item).data('id');
 		if (id === 'save') {
-			this._map.save(true, true);
+			this._map.fire('postMessage', {msgId: 'UI_Save'});
+			if (!this._map._disableDefaultAction['UI_Save']) {
+				this._map.save(true, true);
+			}
 		} else if (id === 'saveas') {
 			this._map.fire('postMessage', {msgId: 'UI_SaveAs'});
 		} else if (id === 'shareas') {
@@ -806,7 +809,9 @@ L.Control.Menubar = L.Control.extend({
 				this._map.fire('postMessage', {msgId: 'close', args: {EverModified: this._map._everModified, Deprecated: true}});
 				this._map.fire('postMessage', {msgId: 'UI_Close', args: {EverModified: this._map._everModified}});
 			}
-			this._map.remove();
+			if (!this._map._disableDefaultAction['UI_Close']) {
+				this._map.remove();
+			}
 		} else if (id === 'repair') {
 			this._map._socket.sendMessage('commandvalues command=.uno:DocumentRepair');
 		} else if (!window.ThisIsAMobileApp && id === 'warn-copy-paste') {
diff --git a/loleaflet/src/control/Control.Toolbar.js b/loleaflet/src/control/Control.Toolbar.js
index e00b5fa98..9e77cb149 100644
--- a/loleaflet/src/control/Control.Toolbar.js
+++ b/loleaflet/src/control/Control.Toolbar.js
@@ -138,7 +138,10 @@ function onClick(e, id, item, subItem) {
 		map.print();
 	}
 	else if (id === 'save') {
-		map.save(false /* An explicit save should terminate cell edit */, false /* An explicit save should save it again */);
+		map.fire('postMessage', {msgId: 'UI_Save'});
+		if (!map._disableDefaultAction['UI_Save']) {
+			map.save(false /* An explicit save should terminate cell edit */, false /* An explicit save should save it again */);
+		}
 	}
 	else if (id === 'repair') {
 		map._socket.sendMessage('commandvalues command=.uno:DocumentRepair');
@@ -300,7 +303,9 @@ function onClick(e, id, item, subItem) {
 			map.fire('postMessage', {msgId: 'close', args: {EverModified: map._everModified, Deprecated: true}});

... etc. - the rest is truncated


More information about the Libreoffice-commits mailing list