[Libreoffice-commits] online.git: test/WhiteBoxTests.cpp wsd/LOOLWSD.cpp wsd/ProxyProtocol.cpp wsd/RequestDetails.cpp wsd/RequestDetails.hpp

Ashod Nakashian (via logerrit) logerrit at kemper.freedesktop.org
Tue Jun 2 18:05:08 UTC 2020


 test/WhiteBoxTests.cpp |  262 +++++++++++++++++++++++++++++++++++++++++++++----
 wsd/LOOLWSD.cpp        |   27 ++---
 wsd/ProxyProtocol.cpp  |   13 --
 wsd/RequestDetails.cpp |  146 ++++++++++++++++++++++-----
 wsd/RequestDetails.hpp |  142 ++++++++++++++++++++++++--
 5 files changed, 513 insertions(+), 77 deletions(-)

New commits:
commit d06ad733c50e2d34cebcae445716fb5110242006
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Mon May 25 10:52:37 2020 -0400
Commit:     Ashod Nakashian <ashnakash at gmail.com>
CommitDate: Tue Jun 2 20:04:48 2020 +0200

    wsd: improved RequestDetails parsing and documentation
    
    ...with support for properly extracting the different
    fields with unit-test.
    
    URIs are quite complex and varied. For historic reasons
    they have all been treated without distinction, which
    makes support for all variants difficult. RequestDetails
    encapsulates this complexity, and now it is almost
    completely documented both descriptively and functionally
    (via extensive unit-tests).
    
    Parsing of the URIs is now more structured by having
    named fields instead of relying on knowing which
    token should contain which field, which is error-prone
    and very opaque.
    
    Change-Id: I68d07c2e00baf43f0ade97d20f62691ffb3bf576
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/95292
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Ashod Nakashian <ashnakash at gmail.com>

diff --git a/test/WhiteBoxTests.cpp b/test/WhiteBoxTests.cpp
index e8290cd2f..bb2f57e4c 100644
--- a/test/WhiteBoxTests.cpp
+++ b/test/WhiteBoxTests.cpp
@@ -879,8 +879,12 @@ void WhiteBoxTests::testRequestDetails_DownloadURI()
 
         RequestDetails details(request, "");
 
+        // LOK_ASSERT_EQUAL(URI, details.getDocumentURI());
+
         LOK_ASSERT_EQUAL(static_cast<std::size_t>(5), details.size());
         LOK_ASSERT_EQUAL(std::string("loleaflet"), details[0]);
+        LOK_ASSERT_EQUAL(std::string("loleaflet"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "loleaflet"));
         LOK_ASSERT(details.equals(0, "loleaflet"));
         LOK_ASSERT_EQUAL(std::string("49c225146"), details[1]);
         LOK_ASSERT_EQUAL(std::string("src"), details[2]);
@@ -897,8 +901,12 @@ void WhiteBoxTests::testRequestDetails_DownloadURI()
 
         RequestDetails details(request, "");
 
+        // LOK_ASSERT_EQUAL(URI, details.getDocumentURI());
+
         LOK_ASSERT_EQUAL(static_cast<std::size_t>(3), details.size());
         LOK_ASSERT_EQUAL(std::string("loleaflet"), details[0]);
+        LOK_ASSERT_EQUAL(std::string("loleaflet"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "loleaflet"));
         LOK_ASSERT(details.equals(0, "loleaflet"));
         LOK_ASSERT_EQUAL(std::string("49c225146"), details[1]);
         LOK_ASSERT_EQUAL(std::string("select2.css"), details[2]);
@@ -921,8 +929,15 @@ void WhiteBoxTests::testRequestDetails_loleafletURI()
 
     RequestDetails details(request, "");
 
+    const std::string wopiSrc
+        = "http://localhost/nextcloud/index.php/apps/richdocuments/wopi/files/593_ocqiesh0cngs";
+
+    LOK_ASSERT_EQUAL(wopiSrc, details.getField(RequestDetails::Field::WOPISrc));
+
     LOK_ASSERT_EQUAL(static_cast<std::size_t>(4), details.size());
     LOK_ASSERT_EQUAL(std::string("loleaflet"), details[0]);
+    LOK_ASSERT_EQUAL(std::string("loleaflet"), details.getField(RequestDetails::Field::Type));
+    LOK_ASSERT(details.equals(RequestDetails::Field::Type, "loleaflet"));
     LOK_ASSERT(details.equals(0, "loleaflet"));
     LOK_ASSERT_EQUAL(std::string("49c225146"), details[1]);
     LOK_ASSERT_EQUAL(std::string("loleaflet.html"), details[2]);
@@ -960,6 +975,7 @@ void WhiteBoxTests::testRequestDetails_local()
 
         const std::string docUri = "file:///home/ash/prj/lo/online/test/data/hello-world.odt";
 
+        LOK_ASSERT_EQUAL(docUri, details.getLegacyDocumentURI());
         LOK_ASSERT_EQUAL(docUri, details.getDocumentURI());
 
         LOK_ASSERT_EQUAL(static_cast<std::size_t>(6), details.size());
@@ -973,9 +989,19 @@ void WhiteBoxTests::testRequestDetails_local()
         LOK_ASSERT_EQUAL(std::string("open"), details[3]);
         LOK_ASSERT_EQUAL(std::string("open"), details[4]);
         LOK_ASSERT_EQUAL(std::string("0"), details[5]);
+
+        LOK_ASSERT_EQUAL(std::string("lool"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "lool"));
+        LOK_ASSERT_EQUAL(std::string("open"), details.getField(RequestDetails::Field::SessionId));
+        LOK_ASSERT(details.equals(RequestDetails::Field::SessionId, "open"));
+        LOK_ASSERT_EQUAL(std::string("open"), details.getField(RequestDetails::Field::Command));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Command, "open"));
+        LOK_ASSERT_EQUAL(std::string("0"), details.getField(RequestDetails::Field::Serial));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Serial, "0"));
     }
 
     {
+        // Blank entries are skipped.
         static const std::string URI = "/lool/"
                                        "file%3A%2F%2F%2Fhome%2Fash%2Fprj%2Flo%2Fonline%2Ftest%"
                                        "2Fdata%2Fhello-world.odt/ws//write/2";
@@ -1006,8 +1032,62 @@ void WhiteBoxTests::testRequestDetails_local()
                 "file%3A%2F%2F%2Fhome%2Fash%2Fprj%2Flo%2Fonline%2Ftest%2Fdata%2Fhello-world.odt"),
             details[1]);
         LOK_ASSERT_EQUAL(std::string("ws"), details[2]);
-        LOK_ASSERT_EQUAL(std::string("write"), details[3]);
-        LOK_ASSERT_EQUAL(std::string("2"), details[4]);
+        LOK_ASSERT_EQUAL(std::string("write"), details[3]); // SessionId, since the real SessionId is blank.
+        LOK_ASSERT_EQUAL(std::string("2"), details[4]); // Command, since SessionId was blank.
+
+        LOK_ASSERT_EQUAL(std::string("lool"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "lool"));
+        LOK_ASSERT_EQUAL(std::string("write"), details.getField(RequestDetails::Field::SessionId));
+        LOK_ASSERT(details.equals(RequestDetails::Field::SessionId, "write"));
+        LOK_ASSERT_EQUAL(std::string("2"), details.getField(RequestDetails::Field::Command));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Command, "2"));
+        LOK_ASSERT_EQUAL(std::string(""), details.getField(RequestDetails::Field::Serial));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Serial, ""));
+    }
+
+    {
+        // Apparently, the initial / can be missing -- all the tests do that.
+        static const std::string URI = "lool/"
+                                       "file%3A%2F%2F%2Fhome%2Fash%2Fprj%2Flo%2Fonline%2Ftest%"
+                                       "2Fdata%2Fhello-world.odt/ws//write/2";
+
+        Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, URI,
+                                       Poco::Net::HTTPMessage::HTTP_1_1);
+        request.setHost(Root);
+        request.set("User-Agent", WOPI_AGENT_STRING);
+        request.set("ProxyPrefix", ProxyPrefix);
+
+        RequestDetails details(request, "");
+        LOK_ASSERT_EQUAL(true, details.isProxy());
+        LOK_ASSERT_EQUAL(ProxyPrefix, details.getProxyPrefix());
+
+        LOK_ASSERT_EQUAL(Root, details.getHostUntrusted());
+        LOK_ASSERT_EQUAL(false, details.isWebSocket());
+        LOK_ASSERT_EQUAL(true, details.isGet());
+
+        const std::string docUri = "file:///home/ash/prj/lo/online/test/data/hello-world.odt";
+
+        LOK_ASSERT_EQUAL(docUri, details.getDocumentURI());
+
+        LOK_ASSERT_EQUAL(static_cast<std::size_t>(5), details.size());
+        LOK_ASSERT_EQUAL(std::string("lool"), details[0]);
+        LOK_ASSERT(details.equals(0, "lool"));
+        LOK_ASSERT_EQUAL(
+            std::string(
+                "file%3A%2F%2F%2Fhome%2Fash%2Fprj%2Flo%2Fonline%2Ftest%2Fdata%2Fhello-world.odt"),
+            details[1]);
+        LOK_ASSERT_EQUAL(std::string("ws"), details[2]);
+        LOK_ASSERT_EQUAL(std::string("write"), details[3]); // SessionId, since the real SessionId is blank.
+        LOK_ASSERT_EQUAL(std::string("2"), details[4]); // Command, since SessionId was blank.
+
+        LOK_ASSERT_EQUAL(std::string("lool"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "lool"));
+        LOK_ASSERT_EQUAL(std::string("write"), details.getField(RequestDetails::Field::SessionId));
+        LOK_ASSERT(details.equals(RequestDetails::Field::SessionId, "write"));
+        LOK_ASSERT_EQUAL(std::string("2"), details.getField(RequestDetails::Field::Command));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Command, "2"));
+        LOK_ASSERT_EQUAL(std::string(""), details.getField(RequestDetails::Field::Serial));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Serial, ""));
     }
 }
 
@@ -1051,7 +1131,11 @@ void WhiteBoxTests::testRequestDetails()
         LOK_ASSERT_EQUAL(false, details.isWebSocket());
         LOK_ASSERT_EQUAL(true, details.isGet());
 
-        const std::string docUri
+        LOK_ASSERT_EQUAL(std::string("b26112ab1b6f2ed98ce1329f0f344791"), details.getField(RequestDetails::Field::SessionId));
+        LOK_ASSERT_EQUAL(std::string("close"), details.getField(RequestDetails::Field::Command));
+        LOK_ASSERT_EQUAL(std::string("31"), details.getField(RequestDetails::Field::Serial));
+
+        const std::string docUri_WopiSrc
             = "http://localhost/nextcloud/index.php/apps/richdocuments/wopi/files/"
               "593_ocqiesh0cngs?access_token=MN0KXXDv9GJ1wCCLnQcjVQT2T7WrfYpA&access_token_ttl=0&"
               "reuse_"
@@ -1064,10 +1148,31 @@ void WhiteBoxTests::testRequestDetails()
               "3DXCookieValue%3ASuperCookieName%3DBAZINGA/ws?WOPISrc=http://localhost/nextcloud/"
               "index.php/apps/richdocuments/wopi/files/593_ocqiesh0cngs&compat=";
 
+        LOK_ASSERT_EQUAL(docUri_WopiSrc, details.getLegacyDocumentURI());
+
+        const std::string docUri
+            = "http://localhost/nextcloud/index.php/apps/richdocuments/wopi/files/"
+              "593_ocqiesh0cngs?access_token=MN0KXXDv9GJ1wCCLnQcjVQT2T7WrfYpA&access_token_ttl=0&"
+              "reuse_"
+              "cookies=oc_sessionPassphrase%"
+              "3D8nFRqycbs7bP97yxCuJviBbVKdCXmuiXp6ZYH0DfUoy5UZDCTQgLwluvbgRbKrdKodJteG3uNE19KNUAoE"
+              "5typ"
+              "f4oBGwJdFY%252F5W9RNST8wEHWkUVIjZy7vmY0ZX38PlS%3Anc_sameSiteCookielax%3Dtrue%3Anc_"
+              "sameSiteCookiestrict%3Dtrue%3Aocqiesh0cngs%3Dr5ujg4tpvgu9paaf5bguiokgjl%"
+              "3AXCookieName%"
+              "3DXCookieValue%3ASuperCookieName%3DBAZINGA";
+
         LOK_ASSERT_EQUAL(docUri, details.getDocumentURI());
 
-        LOK_ASSERT_EQUAL(static_cast<std::size_t>(6), details.size());
+        const std::string wopiSrc
+            = "http://localhost/nextcloud/index.php/apps/richdocuments/wopi/files/593_ocqiesh0cngs";
+
+        LOK_ASSERT_EQUAL(wopiSrc, details.getField(RequestDetails::Field::WOPISrc));
+
+        LOK_ASSERT_EQUAL(static_cast<std::size_t>(8), details.size());
         LOK_ASSERT_EQUAL(std::string("lool"), details[0]);
+        LOK_ASSERT_EQUAL(std::string("lool"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "lool"));
         LOK_ASSERT(details.equals(0, "lool"));
         LOK_ASSERT_EQUAL(
             std::string(
@@ -1078,14 +1183,26 @@ void WhiteBoxTests::testRequestDetails()
                 "19KNUAoE5typf4oBGwJdFY%25252F5W9RNST8wEHWkUVIjZy7vmY0ZX38PlS%253Anc_"
                 "sameSiteCookielax%253Dtrue%253Anc_sameSiteCookiestrict%253Dtrue%"
                 "253Aocqiesh0cngs%253Dr5ujg4tpvgu9paaf5bguiokgjl%253AXCookieName%"
-                "253DXCookieValue%253ASuperCookieName%253DBAZINGA/"
-                "ws?WOPISrc=http%3A%2F%2Flocalhost%2Fnextcloud%2Findex.php%2Fapps%"
-                "2Frichdocuments%2Fwopi%2Ffiles%2F593_ocqiesh0cngs&compat="),
+                "253DXCookieValue%253ASuperCookieName%253DBAZINGA"),
             details[1]);
         LOK_ASSERT_EQUAL(std::string("ws"), details[2]);
-        LOK_ASSERT_EQUAL(std::string("b26112ab1b6f2ed98ce1329f0f344791"), details[3]);
-        LOK_ASSERT_EQUAL(std::string("close"), details[4]);
-        LOK_ASSERT_EQUAL(std::string("31"), details[5]);
+        LOK_ASSERT_EQUAL(
+            std::string("WOPISrc=http%3A%2F%2Flocalhost%2Fnextcloud%2Findex.php%2Fapps%"
+                        "2Frichdocuments%2Fwopi%2Ffiles%2F593_ocqiesh0cngs&compat="),
+            details[3]);
+        LOK_ASSERT_EQUAL(std::string("ws"), details[4]);
+        LOK_ASSERT_EQUAL(std::string("b26112ab1b6f2ed98ce1329f0f344791"), details[5]);
+        LOK_ASSERT_EQUAL(std::string("close"), details[6]);
+        LOK_ASSERT_EQUAL(std::string("31"), details[7]);
+
+        LOK_ASSERT_EQUAL(std::string("lool"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "lool"));
+        LOK_ASSERT_EQUAL(std::string("b26112ab1b6f2ed98ce1329f0f344791"), details.getField(RequestDetails::Field::SessionId));
+        LOK_ASSERT(details.equals(RequestDetails::Field::SessionId, "b26112ab1b6f2ed98ce1329f0f344791"));
+        LOK_ASSERT_EQUAL(std::string("close"), details.getField(RequestDetails::Field::Command));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Command, "close"));
+        LOK_ASSERT_EQUAL(std::string("31"), details.getField(RequestDetails::Field::Serial));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Serial, "31"));
     }
 
     {
@@ -1113,30 +1230,139 @@ void WhiteBoxTests::testRequestDetails()
         LOK_ASSERT_EQUAL(false, details.isWebSocket());
         LOK_ASSERT_EQUAL(true, details.isGet());
 
-        const std::string docUri
+        const std::string docUri_WopiSrc
             = "http://localhost/owncloud/index.php/apps/richdocuments/wopi/files/"
               "165_ocgdpzbkm39u?access_token=ODhIXdJdbsVYQoKKCuaYofyzrovxD3MQ&access_token_ttl=0&"
               "reuse_cookies=XCookieName%3DXCookieValue%3ASuperCookieName%3DBAZINGA/"
               "ws?WOPISrc=http://localhost/owncloud/index.php/apps/richdocuments/wopi/files/"
               "165_ocgdpzbkm39u&compat=";
 
+        LOK_ASSERT_EQUAL(docUri_WopiSrc, details.getLegacyDocumentURI());
+
+        const std::string docUri
+            = "http://localhost/owncloud/index.php/apps/richdocuments/wopi/files/"
+              "165_ocgdpzbkm39u?access_token=ODhIXdJdbsVYQoKKCuaYofyzrovxD3MQ&access_token_ttl=0&"
+              "reuse_cookies=XCookieName%3DXCookieValue%3ASuperCookieName%3DBAZINGA";
+
         LOK_ASSERT_EQUAL(docUri, details.getDocumentURI());
 
-        LOK_ASSERT_EQUAL(static_cast<std::size_t>(6), details.size());
+        const std::string wopiSrc
+            = "http://localhost/owncloud/index.php/apps/richdocuments/wopi/files/"
+              "165_ocgdpzbkm39u";
+
+        LOK_ASSERT_EQUAL(wopiSrc, details.getField(RequestDetails::Field::WOPISrc));
+
+        LOK_ASSERT_EQUAL(static_cast<std::size_t>(8), details.size());
         LOK_ASSERT_EQUAL(std::string("lool"), details[0]);
         LOK_ASSERT(details.equals(0, "lool"));
         LOK_ASSERT_EQUAL(
             std::string("http%3A%2F%2Flocalhost%2Fowncloud%2Findex.php%2Fapps%2Frichdocuments%"
                         "2Fwopi%2Ffiles%2F165_ocgdpzbkm39u%3Faccess_token%"
                         "3DODhIXdJdbsVYQoKKCuaYofyzrovxD3MQ%26access_token_ttl%3D0%26reuse_cookies%"
-                        "3DXCookieName%253DXCookieValue%253ASuperCookieName%253DBAZINGA/"
-                        "ws?WOPISrc=http%3A%2F%2Flocalhost%2Fowncloud%2Findex.php%2Fapps%"
-                        "2Frichdocuments%2Fwopi%2Ffiles%2F165_ocgdpzbkm39u&compat="),
+                        "3DXCookieName%253DXCookieValue%253ASuperCookieName%253DBAZINGA"),
             details[1]);
         LOK_ASSERT_EQUAL(std::string("ws"), details[2]);
-        LOK_ASSERT_EQUAL(std::string("1c99a7bcdbf3209782d7eb38512e6564"), details[3]);
-        LOK_ASSERT_EQUAL(std::string("write"), details[4]);
-        LOK_ASSERT_EQUAL(std::string("2"), details[5]);
+        LOK_ASSERT_EQUAL(
+            std::string("WOPISrc=http%3A%2F%2Flocalhost%2Fowncloud%2Findex.php%2Fapps%"
+                        "2Frichdocuments%2Fwopi%2Ffiles%2F165_ocgdpzbkm39u&compat="),
+            details[3]);
+        LOK_ASSERT_EQUAL(std::string("ws"), details[4]);
+        LOK_ASSERT_EQUAL(std::string("1c99a7bcdbf3209782d7eb38512e6564"), details[5]);
+        LOK_ASSERT_EQUAL(std::string("write"), details[6]);
+        LOK_ASSERT_EQUAL(std::string("2"), details[7]);
+
+        LOK_ASSERT_EQUAL(std::string("lool"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "lool"));
+        LOK_ASSERT_EQUAL(std::string("1c99a7bcdbf3209782d7eb38512e6564"), details.getField(RequestDetails::Field::SessionId));
+        LOK_ASSERT(details.equals(RequestDetails::Field::SessionId, "1c99a7bcdbf3209782d7eb38512e6564"));
+        LOK_ASSERT_EQUAL(std::string("write"), details.getField(RequestDetails::Field::Command));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Command, "write"));
+        LOK_ASSERT_EQUAL(std::string("2"), details.getField(RequestDetails::Field::Serial));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Serial, "2"));
+    }
+
+    {
+        static const std::string URI
+            = "/lool/%2Ftmp%2Fslideshow_b8c3225b_setclientpart.odp/Ar3M1X89mVaryYkh/"
+              "UjaCGP4cYHlU6TvUGdnFTPi8hjOS87uFym7ruWMq3F3jBr0kSPgVhbKz5CwUyV8R/slideshow.svg";
+
+        Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, URI,
+                                       Poco::Net::HTTPMessage::HTTP_1_1);
+        request.setHost(Root);
+        request.set("User-Agent", WOPI_AGENT_STRING);
+        request.set("ProxyPrefix", ProxyPrefix);
+
+        RequestDetails details(request, "");
+        LOK_ASSERT_EQUAL(true, details.isProxy());
+        LOK_ASSERT_EQUAL(ProxyPrefix, details.getProxyPrefix());
+
+        LOK_ASSERT_EQUAL(Root, details.getHostUntrusted());
+        LOK_ASSERT_EQUAL(false, details.isWebSocket());
+        LOK_ASSERT_EQUAL(true, details.isGet());
+
+        const std::string docUri
+            = "/tmp/slideshow_b8c3225b_setclientpart.odp";
+
+        LOK_ASSERT_EQUAL(docUri, details.getLegacyDocumentURI());
+        LOK_ASSERT_EQUAL(docUri, details.getDocumentURI());
+
+        LOK_ASSERT_EQUAL(std::string(), details.getField(RequestDetails::Field::WOPISrc));
+
+        LOK_ASSERT_EQUAL(static_cast<std::size_t>(5), details.size());
+        LOK_ASSERT_EQUAL(std::string("lool"), details[0]);
+        LOK_ASSERT(details.equals(0, "lool"));
+        LOK_ASSERT_EQUAL(std::string("%2Ftmp%2Fslideshow_b8c3225b_setclientpart.odp"), details[1]);
+        LOK_ASSERT_EQUAL(std::string("Ar3M1X89mVaryYkh"), details[2]);
+        LOK_ASSERT_EQUAL(std::string("UjaCGP4cYHlU6TvUGdnFTPi8hjOS87uFym7ruWMq3F3jBr0kSPgVhbKz5CwUyV8R"), details[3]);
+        LOK_ASSERT_EQUAL(std::string("slideshow.svg"), details[4]);
+
+        LOK_ASSERT_EQUAL(std::string("lool"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "lool"));
+        LOK_ASSERT_EQUAL(std::string(""), details.getField(RequestDetails::Field::SessionId));
+        LOK_ASSERT(details.equals(RequestDetails::Field::SessionId, ""));
+        LOK_ASSERT_EQUAL(std::string(""), details.getField(RequestDetails::Field::Command));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Command, ""));
+        LOK_ASSERT_EQUAL(std::string(""), details.getField(RequestDetails::Field::Serial));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Serial, ""));
+    }
+
+    {
+        static const std::string URI = "/lool/"
+                                       "clipboard?WOPISrc=file%3A%2F%2F%2Ftmp%2Fcopypasteef324307_"
+                                       "empty.ods&ServerId=7add98ed&ViewId=0&Tag=5f7972ce4e6a37dd";
+
+        Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_GET, URI,
+                                       Poco::Net::HTTPMessage::HTTP_1_1);
+        request.setHost(Root);
+        request.set("User-Agent", WOPI_AGENT_STRING);
+        request.set("ProxyPrefix", ProxyPrefix);
+
+        RequestDetails details(request, "");
+        LOK_ASSERT_EQUAL(true, details.isProxy());
+        LOK_ASSERT_EQUAL(ProxyPrefix, details.getProxyPrefix());
+
+        LOK_ASSERT_EQUAL(Root, details.getHostUntrusted());
+        LOK_ASSERT_EQUAL(false, details.isWebSocket());
+        LOK_ASSERT_EQUAL(true, details.isGet());
+
+        const std::string docUri = "clipboard";
+
+        LOK_ASSERT_EQUAL(docUri, details.getLegacyDocumentURI());
+        LOK_ASSERT_EQUAL(docUri, details.getDocumentURI());
+
+        LOK_ASSERT_EQUAL(static_cast<std::size_t>(3), details.size());
+        LOK_ASSERT_EQUAL(std::string("lool"), details[0]);
+        LOK_ASSERT(details.equals(0, "lool"));
+        LOK_ASSERT_EQUAL(std::string("clipboard"), details[1]);
+
+        LOK_ASSERT_EQUAL(std::string("lool"), details.getField(RequestDetails::Field::Type));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Type, "lool"));
+        LOK_ASSERT_EQUAL(std::string(""), details.getField(RequestDetails::Field::SessionId));
+        LOK_ASSERT(details.equals(RequestDetails::Field::SessionId, ""));
+        LOK_ASSERT_EQUAL(std::string(""), details.getField(RequestDetails::Field::Command));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Command, ""));
+        LOK_ASSERT_EQUAL(std::string(""), details.getField(RequestDetails::Field::Serial));
+        LOK_ASSERT(details.equals(RequestDetails::Field::Serial, ""));
     }
 }
 
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 917ef33fb..51bd22e38 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2277,7 +2277,7 @@ private:
 
             // re-write ServiceRoot and cache.
             RequestDetails requestDetails(request, LOOLWSD::ServiceRoot);
-//            LOG_TRC("Request details " << requestDetails.toString());
+            // LOG_TRC("Request details " << requestDetails.toString());
 
             // Config & security ...
             if (requestDetails.isProxy())
@@ -2293,14 +2293,14 @@ private:
             {
                 // Unit testing, nothing to do here
             }
-            else if (requestDetails.equals(0, "loleaflet"))
+            else if (requestDetails.equals(RequestDetails::Field::Type, "loleaflet"))
             {
                 // File server
                 assert(socket && "Must have a valid socket");
                 FileServerRequestHandler::handleRequest(request, requestDetails, message, socket);
                 socket->shutdown();
             }
-            else if (requestDetails.equals(0, "lool") &&
+            else if (requestDetails.equals(RequestDetails::Field::Type, "lool") &&
                      requestDetails.equals(1, "adminws"))
             {
                 // Admin connections
@@ -2314,7 +2314,7 @@ private:
                 }
 
             }
-            else if (requestDetails.equals(0, "lool") &&
+            else if (requestDetails.equals(RequestDetails::Field::Type, "lool") &&
                      requestDetails.equals(1, "getMetrics"))
             {
                 // See metrics.txt
@@ -2372,7 +2372,7 @@ private:
             else if (requestDetails.isGet("/robots.txt"))
                 handleRobotsTxtRequest(request, socket);
 
-            else if (requestDetails.equals(0, "lool") &&
+            else if (requestDetails.equals(RequestDetails::Field::Type, "lool") &&
                      requestDetails.equals(1, "clipboard"))
             {
 //              Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
@@ -2382,11 +2382,11 @@ private:
             else if (requestDetails.isProxy() && requestDetails.equals(2, "ws"))
                 handleClientProxyRequest(request, requestDetails, message, disposition);
 
-            else if (requestDetails.equals(0, "lool") &&
+            else if (requestDetails.equals(RequestDetails::Field::Type, "lool") &&
                      requestDetails.equals(2, "ws") && requestDetails.isWebSocket())
                 handleClientWsUpgrade(request, requestDetails, disposition, socket);
 
-            else if (!requestDetails.isWebSocket() && requestDetails.equals(0, "lool"))
+            else if (!requestDetails.isWebSocket() && requestDetails.equals(RequestDetails::Field::Type, "lool"))
             {
                 // All post requests have url prefix 'lool'.
                 handlePostRequest(requestDetails, request, message, disposition, socket);
@@ -2810,9 +2810,10 @@ private:
                 const std::string formName(form.get("name"));
 
                 // Validate the docKey
-                std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
-                std::string decodedUri = requestDetails.getDocumentURI();
+                const std::string decodedUri = requestDetails.getLegacyDocumentURI();
                 const std::string docKey = DocumentBroker::getDocKey(DocumentBroker::sanitizeURI(decodedUri));
+
+                std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
                 auto docBrokerIt = DocBrokers.find(docKey);
 
                 // Maybe just free the client from sending childid in form ?
@@ -2845,8 +2846,9 @@ private:
             // TODO: Check that the user in question has access to this file!
 
             // 1. Validate the dockey
-            std::string decodedUri = requestDetails.getDocumentURI();
+            const std::string decodedUri = requestDetails.getLegacyDocumentURI();
             const std::string docKey = DocumentBroker::getDocKey(DocumentBroker::sanitizeURI(decodedUri));
+
             std::unique_lock<std::mutex> docBrokersLock(DocBrokersMutex);
             auto docBrokerIt = DocBrokers.find(docKey);
             if (docBrokerIt == DocBrokers.end())
@@ -2932,7 +2934,8 @@ private:
                                   Poco::MemoryInputStream& message,
                                   SocketDisposition &disposition)
     {
-        std::string url = requestDetails.getDocumentURI();
+        //FIXME: The DocumentURI includes the WOPISrc, which makes it potentially invalid URI.
+        const std::string url = requestDetails.getLegacyDocumentURI();
 
         LOG_INF("URL [" << url << "].");
         const auto uriPublic = DocumentBroker::sanitizeURI(url);
@@ -3022,7 +3025,7 @@ private:
                                SocketDisposition& disposition,
                                const std::shared_ptr<StreamSocket>& socket)
     {
-        std::string url = requestDetails.getDocumentURI();
+        const std::string url = requestDetails.getLegacyDocumentURI();
         assert(socket && "Must have a valid socket");
 
         // must be trace for anonymization
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index a2b2c75b9..4b6950a10 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -35,12 +35,8 @@ void DocumentBroker::handleProxyRequest(
     const RequestDetails &requestDetails,
     const std::shared_ptr<StreamSocket> &socket)
 {
-    const size_t session = 3;
-    const size_t command = 4;
-    const size_t serial = 5;
-
     std::shared_ptr<ClientSession> clientSession;
-    if (requestDetails.equals(command, "open"))
+    if (requestDetails.equals(RequestDetails::Field::Command, "open"))
     {
         bool isLocal = socket->isLocal();
         LOG_TRC("proxy: validate that socket is from localhost: " << isLocal);
@@ -73,7 +69,7 @@ void DocumentBroker::handleProxyRequest(
     }
     else
     {
-        std::string sessionId = requestDetails[session];
+        const std::string sessionId = requestDetails.getField(RequestDetails::Field::SessionId);
         LOG_TRC("proxy: find session for " << _docKey << " with id " << sessionId);
         for (const auto &it : _sessions)
         {
@@ -98,15 +94,14 @@ void DocumentBroker::handleProxyRequest(
     addSocketToPoll(socket);
 
     auto proxy = std::static_pointer_cast<ProxyProtocolHandler>(protocol);
-    if (requestDetails.equals(command, "close"))
+    if (requestDetails.equals(RequestDetails::Field::Command, "close"))
     {
         LOG_TRC("Close session");
         proxy->notifyDisconnected();
         return;
     }
 
-    (void)serial; // in URL for logging, debugging, and uniqueness.
-    bool isWaiting = requestDetails.equals(command, "wait");
+    const bool isWaiting = requestDetails.equals(RequestDetails::Field::Command, "wait");
     proxy->handleRequest(isWaiting, socket);
 }
 
diff --git a/wsd/RequestDetails.cpp b/wsd/RequestDetails.cpp
index ce8e51bd1..7118a170d 100644
--- a/wsd/RequestDetails.cpp
+++ b/wsd/RequestDetails.cpp
@@ -10,10 +10,39 @@
 #include <config.h>
 
 #include "RequestDetails.hpp"
+#include "common/Log.hpp"
 
 #include <Poco/URI.h>
 #include "Exceptions.hpp"
 
+/// Returns true iff the two containers are equal.
+template <typename T> bool equal(const T& lhs, const T& rhs)
+{
+    if (lhs.size() != rhs.size())
+    {
+        LOG_ERR("!!! Size mismatch: [" << lhs.size() << "] != [" << rhs.size() << "].");
+        return false;
+    }
+
+    const auto endLeft = std::end(lhs);
+
+    auto itRight = std::begin(rhs);
+
+    for (auto itLeft = std::begin(lhs); itLeft != endLeft; ++itLeft, ++itRight)
+    {
+        const auto subLeft = lhs.getParam(*itLeft);
+        const auto subRight = rhs.getParam(*itRight);
+
+        if (subLeft != subRight)
+        {
+            LOG_ERR("!!! Data mismatch: [" << subLeft << "] != [" << subRight << "]");
+            return false;
+        }
+    }
+
+    return true;
+}
+
 RequestDetails::RequestDetails(Poco::Net::HTTPRequest &request, const std::string& serviceRoot)
     : _isMobile(false)
 {
@@ -39,6 +68,19 @@ RequestDetails::RequestDetails(Poco::Net::HTTPRequest &request, const std::strin
 	_hostUntrusted = request.getHost();
 #endif
 
+    // Poco::SyntaxException is thrown when the syntax is invalid.
+    Poco::URI uri(_uriString);
+    for (const auto& param : uri.getQueryParameters())
+    {
+        LOG_TRC("Decoding param [" << param.first << "] = [" << param.second << "].");
+
+        std::string value;
+        Poco::URI::decode(param.second, value);
+
+        _params.emplace(param.first, value);
+    }
+
+    // First tokenize by '/' then by '?'.
     std::vector<StringToken> tokens;
     const auto len = _uriString.size();
     if (len > 0)
@@ -48,21 +90,6 @@ RequestDetails::RequestDetails(Poco::Net::HTTPRequest &request, const std::strin
         {
             if (_uriString[i] == '/' || _uriString[i] == '?')
             {
-                if (_uriString[i] == '/')
-                {
-                    // Wopi also uses /ws? in the URL, which
-                    // we need to avoid confusing with the
-                    // trailing /ws/<command>/<sessionId>/<serial>.
-                    // E.g. /ws?WOPISrc=
-                    if (i + 3 < len && _uriString[i + 1] == 'w' && _uriString[i + 2] == 's'
-                        && _uriString[i + 3] == '?')
-                    {
-                        // Skip over '/ws?'
-                        i += 4;
-                        continue;
-                    }
-                }
-
                 if (i - start > 0) // ignore empty
                     tokens.emplace_back(start, i - start);
                 start = i + 1;
@@ -72,6 +99,84 @@ RequestDetails::RequestDetails(Poco::Net::HTTPRequest &request, const std::strin
             tokens.emplace_back(start, i - start);
         _pathSegs = StringVector(_uriString, std::move(tokens));
     }
+
+
+    std::size_t off = 0;
+    std::size_t posDocUri = _uriString.find_first_of('/');
+    if (posDocUri == 0)
+    {
+        off = 1;
+        posDocUri = _uriString.find_first_of('/', 1);
+    }
+
+    _fields[Field::Type] = _uriString.substr(off, posDocUri - off); // The first is always the type.
+    std::string uriRes = _uriString.substr(posDocUri + 1);
+
+    const auto posLastWS = uriRes.rfind("/ws");
+    // DocumentURI is the second segment in lool URIs.
+    if (_pathSegs.equals(0, "lool"))
+    {
+        //FIXME: For historic reasons the DocumentURI includes the WOPISrc.
+        // This is problematic because decoding a URI that embedds not one, but
+        // *two* encoded URIs within it is bound to produce an invalid URI.
+        // Potentially three '?' might exist in the result (after decoding).
+        std::size_t end = uriRes.rfind("/ws?");
+        if (end != std::string::npos)
+        {
+            // Until the end of the WOPISrc.
+            // e.g. <encoded-document-URI+options>/ws?WOPISrc=<encoded-document-URI>&compat=
+            end = uriRes.find_first_of("/?", end + 4, 2); // Start searching after '/ws?'.
+        }
+        else
+        {
+            end = (posLastWS != std::string::npos ? posLastWS : uriRes.find('/'));
+            if (end == std::string::npos)
+                end = uriRes.find('?'); // e.g. /lool/clipboard?WOPISrc=file%3A%2F%2F%2Ftmp%2Fcopypasteef324307_empty.ods...
+        }
+
+        const std::string docUri = uriRes.substr(0, end);
+
+        std::string decoded;
+        Poco::URI::decode(docUri, decoded);
+        _fields[Field::LegacyDocumentURI] = decoded;
+
+        // Find the DocumentURI proper.
+        end = uriRes.find_first_of("/?", 0, 2);
+        decoded.clear();
+        Poco::URI::decode(uriRes.substr(0, end), decoded);
+        _fields[Field::DocumentURI] = decoded;
+    }
+    else // Otherwise, it's the full URI.
+    {
+        _fields[Field::LegacyDocumentURI] = _uriString;
+        _fields[Field::DocumentURI] = _uriString;
+    }
+
+    _fields[Field::WOPISrc] = getParam("WOPISrc");
+
+    // &compat=
+    const std::string compat = getParam("compat");
+    if (!compat.empty())
+        _fields[Field::Compat] = compat;
+
+    // /ws[/<sessionId>/<command>/<serial>]
+    if (posLastWS != std::string::npos)
+    {
+        std::string lastWS = uriRes.substr(posLastWS);
+        const auto proxyTokens = Util::tokenize(lastWS, '/');
+        if (proxyTokens.size() > 1)
+        {
+            _fields[Field::SessionId] = proxyTokens[1];
+            if (proxyTokens.size() > 2)
+            {
+                _fields[Field::Command] = proxyTokens[2];
+                if (proxyTokens.size() > 3)
+                {
+                    _fields[Field::Serial] = proxyTokens[3];
+                }
+            }
+        }
+    }
 }
 
 RequestDetails::RequestDetails(const std::string &mobileURI)
@@ -84,15 +189,4 @@ RequestDetails::RequestDetails(const std::string &mobileURI)
     _uriString = mobileURI;
 }
 
-std::string RequestDetails::getDocumentURI() const
-{
-    if (_isMobile)
-        return _uriString;
-
-    assert(equals(0, "lool"));
-    std::string docURI;
-    Poco::URI::decode(_pathSegs[1], docURI);
-    return docURI;
-}
-
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/RequestDetails.hpp b/wsd/RequestDetails.hpp
index b931b98f8..695db11cc 100644
--- a/wsd/RequestDetails.hpp
+++ b/wsd/RequestDetails.hpp
@@ -13,13 +13,100 @@
 
 #include <common/StringVector.hpp>
 #include <common/Util.hpp>
+#include <common/Log.hpp>
 
 /**
  * A class to encapsulate various useful pieces from the request.
  * as well as path parsing goodness.
+ *
+ * The URI is complex and encapsulates multiple segments with
+ * different purposes and consumers. There are three URIs
+ * formats/modes that are supported.
+ *
+ * literal URI: used by ConvertToBroker.
+ * Origin: ConvertToBroker::startConversion
+ * Format:
+ *  <anything>
+ * Identifier: special constructor that takes a string.
+ * Example:
+ *  convert-to
+ *
+ * loleaflet URI: used to load loleaflet.html and other static files.
+ * Origin: the page where the document will be embedded.
+ * Format:
+ *  /loleaflet/<loolwsd-version-hash>/[path/]<filename>.<ext>[?WOPISrc=<encoded-document-URI>]
+ * Identifier: /loleaflet/.
+ * Examples:
+ *  /loleaflet/49c225146/src/map/Clipboard.js
+ *  /loleaflet/49c225146/loleaflet.html?WOPISrc=http%3A%2F%2Flocalhost%2Fnextcloud%2Findex.php%2Fapps%2Frichdocuments%2Fwopi%2Ffiles%2F593_ocqiesh0cngs&title=empty.odt&lang=en-us&closebutton=1&revisionhistory=1
+ *
+ * lool URI: used to load the document.
+ * Origin: loleaflet.html
+ * Format:
+ *  /lool/<encoded-document-URI+options>/ws?WOPISrc=<encoded-document-URI>&compat=/ws[/<sessionId>/<command>/<serial>]
+ * Identifier: /lool/.
+ *
+ * The 'document-URI' is the original URL in the client that is used to load the document page.
+ * The optional section at the end, in square-brackets, is for richproxy.
+ *
+ * Example:
+ *  /lool/http%3A%2F%2Flocalhost%2Fowncloud%2Findex.php%2Fapps%2Frichdocuments%2Fwopi%2Ffiles%2F165_ocgdpzbkm39u%3F
+ *  access_token%3DODhIXdJdbsVYQoKKCuaYofyzrovxD3MQ%26access_token_ttl%3D0%26reuse_cookies%3DXCookieName%253DXCookieValue%253
+ *  ASuperCookieName%253DBAZINGA/ws?WOPISrc=http%3A%2F%2Flocalhost%2Fowncloud%2Findex.php%2Fapps%2Frichdocuments%2Fwopi%2F
+ *  files%2F165_ocgdpzbkm39u&compat=/ws/1c99a7bcdbf3209782d7eb38512e6564/write/2
+ *  Where:
+ *      encoded-document-URI+options:
+ *          http%3A%2F%2Flocalhost%2Fowncloud%2Findex.php%2Fapps%2Frichdocuments%2Fwopi%2Ffiles%2F165_ocgdpzbkm39u%3F
+ *          access_token%3DODhIXdJdbsVYQoKKCuaYofyzrovxD3MQ%26access_token_ttl%3D0%26reuse_cookies%3DXCookieName%253DXCookieValue%253
+ *          ASuperCookieName%253DBAZINGA
+ *      encoded-document-URI:
+ *          http%3A%2F%2Flocalhost%2Fowncloud%2Findex.php%2Fapps%2Frichdocuments%2Fwopi%2Ffiles%2F165_ocgdpzbkm39u
+ *      sessionId:
+ *          1c99a7bcdbf3209782d7eb38512e6564
+ *      command:
+ *          write
+ *      serial:
+ *          2
+ *  In decoded form:
+ *      document-URI+options:
+ *          http://localhost/owncloud/index.php/apps/richdocuments/wopi/files/165_ocgdpzbkm39u?access_token=
+ *          ODhIXdJdbsVYQoKKCuaYofyzrovxD3MQ&access_token_ttl=0&reuse_cookies=XCookieName%3DXCookieValue%3ASuperCookieName%3DBAZINGA
+ *      document-URI:
+ *          http://localhost/owncloud/index.php/apps/richdocuments/wopi/files/165_ocgdpzbkm39u
+ *
+ * Note that the options are still encoded and need decoding separately.
+ *
+ * Due to the multi-layer nature of the URI, it raises many difficulties, not least
+ * the fact that it has multiple query parameters ('?' sections). It also has foreslash
+ * delimiters after query parameters.
+ *
+ * The different sections are henceforth given names to help both in documenting and
+ * communicating them, and to facilitate parsing them.
+ *
+ * /lool/<encoded-document-URI+options>/ws?WOPISrc=<encoded-document-URI>&compat=/ws[/<sessionId>/<command>/<serial>]
+ *       |--------documentURI---------|            |-------WOPISrc------|        |--------------compat--------------|
+ *                            |options|                                               |sessionId| |command| |serial|
+ *       |---------------------------LegacyDocumentURI---------------------------|
  */
 class RequestDetails
 {
+public:
+
+    /// The fields of the URI.
+    enum class Field
+    {
+        Type,
+        DocumentURI,
+        LegacyDocumentURI, //< Legacy, to be removed.
+        WOPISrc,
+        Compat,
+        SessionId,
+        Command,
+        Serial
+    };
+
+private:
+
     bool _isGet : 1;
     bool _isHead : 1;
     bool _isProxy : 1;
@@ -30,12 +117,21 @@ class RequestDetails
     std::string _hostUntrusted;
     std::string _documentURI;
     StringVector _pathSegs;
+    std::map<std::string, std::string> _params;
+    std::map<Field, std::string> _fields;
+
 public:
+
     RequestDetails(Poco::Net::HTTPRequest &request, const std::string& serviceRoot);
     RequestDetails(const std::string &mobileURI);
+
     // matches the WOPISrc if used. For load balancing
     // must be 2nd element in the path after /lool/<here>
-    std::string getDocumentURI() const;
+    std::string getLegacyDocumentURI() const { return getField(Field::LegacyDocumentURI); }
+
+    /// The DocumentURI, decoded. Doesn't contain WOPISrc or any other appendages.
+    std::string getDocumentURI() const { return getField(Field::DocumentURI); }
+
     std::string getURI() const
     {
         return _uriString;
@@ -68,32 +164,54 @@ public:
     {
         return (_isGet || _isHead) && _uriString == path;
     }
-    bool startsWith(const char *path)
-    {
-        return Util::startsWith(_uriString, path);
-    }
-    bool equals(size_t index, const char *string) const
+
+    bool equals(std::size_t index, const char* string) const
     {
         return _pathSegs.equals(index, string);
     }
-    std::string operator[](size_t index) const
+
+    /// Return the segment of the URI at index.
+    /// URI segments are delimited by '/'.
+    std::string operator[](std::size_t index) const
     {
         return _pathSegs[index];
     }
-    size_t size() const
+
+    /// Returns the number of segments in the URI.
+    std::size_t size() const
     {
         return _pathSegs.size();
     }
+
+    std::string getParam(const std::string& name) const
+    {
+        const auto it = _params.find(name);
+        return it != _params.end() ? it->second : std::string();
+    }
+
+    std::string getField(const Field field) const
+    {
+        const auto it = _fields.find(field);
+        return it != _fields.end() ? it->second : std::string();
+    }
+
+    bool equals(const Field field, const char* string) const
+    {
+        const auto it = _fields.find(field);
+        return it != _fields.end() ? it->second == string : (string == nullptr || *string == '\0');
+    }
+
     std::string toString() const
     {
         std::ostringstream oss;
         oss << _uriString << ' ' << (_isGet?"G":"")
             << (_isHead?"H":"") << (_isProxy?"Proxy":"")
             << (_isWebSocket?"WebSocket":"");
-        oss << " host: " << _hostUntrusted;
-        oss << " path: " << _pathSegs.size();
-        for (size_t i = 0; i < _pathSegs.size(); ++i)
-            oss << " '" << _pathSegs[i] << "'";
+        oss << ", host: " << _hostUntrusted;
+        oss << ", path: " << _pathSegs.size();
+        for (std::size_t i = 0; i < _pathSegs.size(); ++i)
+            oss << "\n[" << i << "] '" << _pathSegs[i] << "'";
+        oss << "\nfull URI: " << _uriString;
         return oss.str();
     }
 };


More information about the Libreoffice-commits mailing list