/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
 
#include "CurlSession.hxx"
 
#include "SerfLockStore.hxx"
#include "DAVProperties.hxx"
#include "UCBDeadPropertyValue.hxx"
#include "webdavresponseparser.hxx"
 
#include <cppuhelper/implbase.hxx>
#include <comphelper/processfactory.hxx>
#include <comphelper/attributelist.hxx>
#include <comphelper/scopeguard.hxx>
#include <comphelper/string.hxx>
#include <cppuhelper/queryinterface.hxx>
#include <cppuhelper/supportsservice.hxx>
 
#include <o3tl/safeint.hxx>
#include <o3tl/string_view.hxx>
 
#include <officecfg/Inet.hxx>
#include <officecfg/Office/Security.hxx>
 
#include <com/sun/star/beans/NamedValue.hpp>
#include <com/sun/star/io/Pipe.hpp>
#include <com/sun/star/lang/XServiceInfo.hpp>
#include <com/sun/star/io/SequenceInputStream.hpp>
#include <com/sun/star/io/SequenceOutputStream.hpp>
#include <com/sun/star/xml/sax/Writer.hpp>
 
#include <osl/time.h>
#include <sal/log.hxx>
#include <rtl/uri.hxx>
#include <rtl/strbuf.hxx>
#include <rtl/ustrbuf.hxx>
#include <systools/curlinit.hxx>
#include <tools/hostfilter.hxx>
#include <config_version.h>
 
#include <map>
#include <optional>
#include <tuple>
#include <utility>
 
using namespace ::com::sun::star;
 
namespace
{
void lock_cb(CURL*, curl_lock_data, curl_lock_access, void*);
void unlock_cb(CURL*, curl_lock_data, void*);
 
/// globals container
struct Init
{
    /// note: LockStore has its own mutex and calls CurlSession from its thread
    ///       so don't call LockStore with m_Mutex held to prevent deadlock.
    ::http_dav_ucp::SerfLockStore LockStore;
 
    /// libcurl shared data - to store cookies beyond one connection
    ::std::mutex ShareLock[CURL_LOCK_DATA_LAST];
    ::std::unique_ptr<CURLSH, http_dav_ucp::deleter_from_fn<CURLSH, curl_share_cleanup>> pShare;
 
    Init()
    {
        if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK)
        {
            assert(!"curl_global_init failed");
            ::std::abort(); // can't handle error here
        }
        pShare.reset(curl_share_init());
        if (!pShare)
        {
            assert(!"curl_share_init failed");
            ::std::abort(); // can't handle error here
        }
        CURLSHcode sh = curl_share_setopt(pShare.get(), CURLSHOPT_LOCKFUNC, lock_cb);
        if (sh != CURLSHE_OK)
        {
            assert(!"curl_share_setopt failed");
            ::std::abort(); // can't handle error here
        }
        sh = curl_share_setopt(pShare.get(), CURLSHOPT_UNLOCKFUNC, unlock_cb);
        if (sh != CURLSHE_OK)
        {
            assert(!"curl_share_setopt failed");
            ::std::abort(); // can't handle error here
        }
        sh = curl_share_setopt(pShare.get(), CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
        if (sh != CURLSHE_OK)
        {
            assert(!"curl_share_setopt failed");
            ::std::abort(); // can't handle error here
        }
        sh = curl_share_setopt(pShare.get(), CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
        // might fail but this is just a perf improvement
        SAL_WARN_IF(sh != CURLSHE_OK, "ucb.ucp.webdav.curl", "curl_share_setopt failed");
        sh = curl_share_setopt(pShare.get(), CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
        // might fail but this is just a perf improvement
        SAL_WARN_IF(sh != CURLSHE_OK, "ucb.ucp.webdav.curl", "curl_share_setopt failed");
        // note: CURL_LOCK_DATA_CONNECT isn't safe in a multi threaded program.
    }
    // do not call curl_global_cleanup() - this is not the only client of curl
};
Init g_Init;
 
// global callbacks
 
void lock_cb(CURL* /*handle*/, curl_lock_data const data, curl_lock_access /*access*/,
             void* /*userptr*/)
{
    assert(0 <= data && data < CURL_LOCK_DATA_LAST);
    try
    {
        g_Init.ShareLock[data].lock();
    }
    catch (std::exception const&)
    {
        ::std::abort();
    }
}
 
void unlock_cb(CURL* /*handle*/, curl_lock_data const data, void* /*userptr*/)
{
    assert(0 <= data && data < CURL_LOCK_DATA_LAST);
    g_Init.ShareLock[data].unlock();
}
 
struct ResponseHeaders
{
    ::std::vector<::std::pair<::std::vector<OString>, ::std::optional<long>>> HeaderFields;
    CURL* pCurl;
    ResponseHeaders(CURL* const i_pCurl)
        : pCurl(i_pCurl)
    {
    }
};
 
auto GetResponseCode(ResponseHeaders const& rHeaders) -> ::std::optional<long>
{
    return (rHeaders.HeaderFields.empty()) ? ::std::optional<long>{}
                                           : rHeaders.HeaderFields.back().second;
}
 
struct DownloadTarget
{
    uno::Reference<io::XOutputStream> xOutStream;
    ResponseHeaders const& rHeaders;
    DownloadTarget(uno::Reference<io::XOutputStream> i_xOutStream,
                   ResponseHeaders const& i_rHeaders)
        : xOutStream(std::move(i_xOutStream))
        , rHeaders(i_rHeaders)
    {
    }
};
 
struct UploadSource
{
    uno::Sequence<sal_Int8> const& rInData;
    size_t nPosition;
    UploadSource(uno::Sequence<sal_Int8> const& i_rInData)
        : rInData(i_rInData)
        , nPosition(0)
    {
    }
};
 
auto GetErrorString(CURLcode const rc, char const* const pErrorBuffer = nullptr) -> OString
{
    char const* const pMessage( // static fallback
        (pErrorBuffer && pErrorBuffer[0] != '\0') ? pErrorBuffer : curl_easy_strerror(rc));
    return OString::Concat("(") + OString::number(sal_Int32(rc)) + ") " + pMessage;
}
 
auto GetErrorStringMulti(CURLMcode const mc) -> OString
{
    return OString::Concat("(") + OString::number(sal_Int32(mc)) + ") " + curl_multi_strerror(mc);
}
 
/// represent an option to be passed to curl_easy_setopt()
struct CurlOption
{
    CURLoption const Option;
    enum class Type
    {
        Pointer,
        Long,
        CurlOffT
    };
    Type const Tag;
    union {
        void const* const pValue;
        long /*const*/ lValue;
        curl_off_t /*const*/ cValue;
    };
    char const* const pExceptionString;
 
    CurlOption(CURLoption const i_Option, void const* const i_Value,
               char const* const i_pExceptionString)
        : Option(i_Option)
        , Tag(Type::Pointer)
        , pValue(i_Value)
        , pExceptionString(i_pExceptionString)
    {
    }
    // Depending on platform, curl_off_t may be "long" or a larger type
    // so cannot use overloading to distinguish these cases.
    CurlOption(CURLoption const i_Option, curl_off_t const i_Value,
               char const* const i_pExceptionString, Type const type = Type::Long)
        : Option(i_Option)
        , Tag(type)
        , pExceptionString(i_pExceptionString)
    {
        static_assert(sizeof(long) <= sizeof(curl_off_t));
        switch (type)
        {
            case Type::Long:
                lValue = i_Value;
                break;
            case Type::CurlOffT:
                cValue = i_Value;
                break;
            default:
                assert(false);
        }
    }
};
 
/// combined guard class to ensure things are released in correct order,
/// particularly in ProcessRequest() error handling
class Guard
{
private:
    /// mutex *first* because m_oGuard requires it
    ::std::unique_lock<::std::mutex> m_Lock;
    ::std::vector<CurlOption> const m_Options;
    ::http_dav_ucp::CurlUri const& m_rURI;
    CURL* const m_pCurl;
 
public:
    explicit Guard(::std::mutex& rMutex, ::std::vector<CurlOption> aOptions,
                   ::http_dav_ucp::CurlUri const& rURI, CURL* const pCurl)
        : m_Lock(rMutex, ::std::defer_lock)
        , m_Options(std::move(aOptions))
        , m_rURI(rURI)
        , m_pCurl(pCurl)
    {
        Acquire();
    }
    ~Guard()
    {
        if (m_Lock.owns_lock())
        {
            Release();
        }
    }
 
    void Acquire()
    {
        assert(!m_Lock.owns_lock());
        m_Lock.lock();
        for (auto const& it : m_Options)
        {
            CURLcode rc(CURL_LAST); // warning C4701
            if (it.Tag == CurlOption::Type::Pointer)
            {
                rc = curl_easy_setopt(m_pCurl, it.Option, it.pValue);
            }
            else if (it.Tag == CurlOption::Type::Long)
            {
                rc = curl_easy_setopt(m_pCurl, it.Option, it.lValue);
            }
            else if (it.Tag == CurlOption::Type::CurlOffT)
            {
                rc = curl_easy_setopt(m_pCurl, it.Option, it.cValue);
            }
            else
            {
                assert(false);
            }
            if (it.pExceptionString != nullptr)
            {
                if (rc != CURLE_OK)
                {
                    SAL_WARN("ucb.ucp.webdav.curl",
                             "set " << it.pExceptionString << " failed: " << GetErrorString(rc));
                    throw ::http_dav_ucp::DAVException(
                        ::http_dav_ucp::DAVException::DAV_SESSION_CREATE,
                        ::http_dav_ucp::ConnectionEndPointString(m_rURI.GetHost(),
                                                                 m_rURI.GetPort()));
                }
            }
            else // many of the options cannot fail
            {
                assert(rc == CURLE_OK);
            }
        }
    }
    void Release()
    {
        assert(m_Lock.owns_lock());
        for (auto const& it : m_Options)
        {
            CURLcode rc(CURL_LAST); // warning C4701
            if (it.Tag == CurlOption::Type::Pointer)
            {
                rc = curl_easy_setopt(m_pCurl, it.Option, nullptr);
            }
            else if (it.Tag == CurlOption::Type::Long)
            {
                rc = curl_easy_setopt(m_pCurl, it.Option, 0L);
            }
            else if (it.Tag == CurlOption::Type::CurlOffT)
            {
                rc = curl_easy_setopt(m_pCurl, it.Option, curl_off_t(-1));
            }
            else
            {
                assert(false);
            }
            assert(rc == CURLE_OK);
            (void)rc;
        }
        m_Lock.unlock();
    }
};
 
} // namespace
 
namespace http_dav_ucp
{
// libcurl callbacks:
 
static int debug_callback(CURL* handle, curl_infotype type, char* data, size_t size,
                          void* /*userdata*/)
{
    char const* pType(nullptr);
    switch (type)
    {
        case CURLINFO_TEXT:
            SAL_INFO("ucb.ucp.webdav.curl", "debug log: " << handle << ": " << data);
            return 0;
        case CURLINFO_HEADER_IN:
            SAL_INFO("ucb.ucp.webdav.curl",
                     "CURLINFO_HEADER_IN: " << handle << ": " << OString(data, size));
            return 0;
        case CURLINFO_HEADER_OUT:
        {
            // unlike IN, this is all headers in one call
            OString tmp(data, size);
            sal_Int32 const start(tmp.indexOf("Authorization: "));
            if (start != -1)
            {
                sal_Int32 const end(tmp.indexOf("\r\n", start));
                assert(end != -1);
                sal_Int32 const len(SAL_N_ELEMENTS("Authorization: ") - 1);
                tmp = tmp.replaceAt(
                    start + len, end - start - len,
                    Concat2View(OString::number(end - start - len) + " bytes redacted"));
            }
            SAL_INFO("ucb.ucp.webdav.curl", "CURLINFO_HEADER_OUT: " << handle << ": " << tmp);
            return 0;
        }
        case CURLINFO_DATA_IN:
            pType = "CURLINFO_DATA_IN";
            break;
        case CURLINFO_DATA_OUT:
            pType = "CURLINFO_DATA_OUT";
            break;
        case CURLINFO_SSL_DATA_IN:
            pType = "CURLINFO_SSL_DATA_IN";
            break;
        case CURLINFO_SSL_DATA_OUT:
            pType = "CURLINFO_SSL_DATA_OUT";
            break;
        default:
            SAL_WARN("ucb.ucp.webdav.curl", "unexpected debug log type");
            return 0;
    }
    SAL_INFO("ucb.ucp.webdav.curl", "debug log: " << handle << ": " << pType << " " << size);
    return 0;
}
 
static size_t write_callback(char* const ptr, size_t const size, size_t const nmemb,
                             void* const userdata)
{
    auto* const pTarget(static_cast<DownloadTarget*>(userdata));
    if (!pTarget) // looks like ~every request may have a response body
    {
        return nmemb;
    }
    assert(size == 1); // says the man page
    (void)size;
    assert(pTarget->xOutStream.is());
    auto const oResponseCode(GetResponseCode(pTarget->rHeaders));
    if (!oResponseCode)
    {
        return 0; // that is an error
    }
    // always write, for exception handler in ProcessRequest()
    //    if (200 <= *oResponseCode && *oResponseCode < 300)
    {
        try
        {
            uno::Sequence<sal_Int8> const data(reinterpret_cast<sal_Int8*>(ptr), nmemb);
            pTarget->xOutStream->writeBytes(data);
        }
        catch (...)
        {
            SAL_WARN("ucb.ucp.webdav.curl", "exception in write_callback");
            return 0; // error
        }
    }
    // else: ignore the body? CurlSession will check the status eventually
    return nmemb;
}
 
static size_t read_callback(char* const buffer, size_t const size, size_t const nitems,
                            void* const userdata)
{
    auto* const pSource(static_cast<UploadSource*>(userdata));
    assert(pSource);
    size_t const nBytes(size * nitems);
    size_t nRet(0);
    try
    {
        assert(pSource->nPosition <= o3tl::make_unsigned(pSource->rInData.getLength()));
        nRet = ::std::min<size_t>(pSource->rInData.getLength() - pSource->nPosition, nBytes);
        ::std::memcpy(buffer, pSource->rInData.getConstArray() + pSource->nPosition, nRet);
        pSource->nPosition += nRet;
    }
    catch (...)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "exception in read_callback");
        return CURL_READFUNC_ABORT; // error
    }
    return nRet;
}
 
static size_t header_callback(char* const buffer, size_t const size, size_t const nitems,
                              void* const userdata)
{
    auto* const pHeaders(static_cast<ResponseHeaders*>(userdata));
    assert(pHeaders);
#if 0
    if (!pHeaders) // TODO maybe not needed in every request? not sure
    {
        return nitems;
    }
#endif
    assert(size == 1); // says the man page
    (void)size;
    try
    {
        if (nitems <= 2)
        {
            // end of header, body follows...
            if (pHeaders->HeaderFields.empty())
            {
                SAL_WARN("ucb.ucp.webdav.curl", "header_callback: empty header?");
                return 0; // error
            }
            // unfortunately there's no separate callback when the body begins,
            // so have to manually retrieve the status code here
            long statusCode(SC_NONE);
            auto rc = curl_easy_getinfo(pHeaders->pCurl, CURLINFO_RESPONSE_CODE, &statusCode);
            assert(rc == CURLE_OK);
            (void)rc;
            // always put the current response code here - wasn't necessarily in this header
            pHeaders->HeaderFields.back().second.emplace(statusCode);
        }
        else if (buffer[0] == ' ' || buffer[0] == '\t') // folded header field?
        {
            size_t i(0);
            do
            {
                ++i;
            } while (i == ' ' || i == '\t');
            if (pHeaders->HeaderFields.empty() || pHeaders->HeaderFields.back().second
                || pHeaders->HeaderFields.back().first.empty())
            {
                SAL_WARN("ucb.ucp.webdav.curl",
                         "header_callback: folded header field without start");
                return 0; // error
            }
            pHeaders->HeaderFields.back().first.back()
                += OString::Concat(" ") + ::std::string_view(&buffer[i], nitems - i);
        }
        else
        {
            if (pHeaders->HeaderFields.empty() || pHeaders->HeaderFields.back().second)
            {
                pHeaders->HeaderFields.emplace_back();
            }
            pHeaders->HeaderFields.back().first.emplace_back(OString(buffer, nitems));
        }
    }
    catch (...)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "exception in header_callback");
        return 0; // error
    }
    return nitems;
}
 
static auto ProcessHeaders(::std::vector<OString> const& rHeaders) -> ::std::map<OUString, OUString>
{
    ::std::map<OUString, OUString> ret;
    for (OString const& rLine : rHeaders)
    {
        std::string_view line;
        if (!rLine.endsWith("\r\n", &line))
        {
            SAL_WARN("ucb.ucp.webdav.curl", "invalid header field (no CRLF)");
            continue;
        }
        if (o3tl::starts_with(line, "HTTP/") // first line
            || line.empty()) // last line
        {
            continue;
        }
        const std::string_view::size_type nColon(line.find(':'));
        if (nColon == std::string_view::npos)
        {
            {
                SAL_WARN("ucb.ucp.webdav.curl", "invalid header field (no :)");
            }
            continue;
        }
        if (nColon == 0)
        {
            SAL_WARN("ucb.ucp.webdav.curl", "invalid header field (empty name)");
            continue;
        }
        assert(nColon != std::string_view::npos);
        // case insensitive; must be ASCII
        auto const name(::rtl::OStringToOUString(OString(line.substr(0, nColon)).toAsciiLowerCase(),
                                                 RTL_TEXTENCODING_ASCII_US));
        std::string_view::size_type nStart(nColon + 1);
        while (nStart < line.size() && (line[nStart] == ' ' || line[nStart] == '\t'))
        {
            ++nStart;
        }
        std::string_view::size_type nEnd(line.size());
        while (nStart < nEnd && (line[nEnd - 1] == ' ' || line[nEnd - 1] == '\t'))
        {
            --nEnd;
        }
        // RFC 7230 says that only ASCII works reliably anyway (neon also did this)
        auto const value(::rtl::OStringToOUString(line.substr(nStart, nEnd - nStart),
                                                  RTL_TEXTENCODING_ASCII_US));
        auto const it(ret.find(name));
        if (it != ret.end())
        {
            it->second = it->second + "," + value;
        }
        else
        {
            ret[name] = value;
        }
    }
    return ret;
}
 
static auto ExtractRequestedHeaders(
    ResponseHeaders const& rHeaders,
    ::std::pair<::std::vector<OUString> const&, DAVResource&> const* const pRequestedHeaders)
    -> void
{
    ::std::map<OUString, OUString> const headerMap(
        ProcessHeaders(rHeaders.HeaderFields.back().first));
    if (pRequestedHeaders)
    {
        for (OUString const& rHeader : pRequestedHeaders->first)
        {
            auto const it(headerMap.find(rHeader.toAsciiLowerCase()));
            if (it != headerMap.end())
            {
                DAVPropertyValue value;
                value.IsCaseSensitive = false;
                value.Name = it->first;
                value.Value <<= it->second;
                pRequestedHeaders->second.properties.push_back(value);
            }
        }
    }
}
 
// this appears to be the only way to get the "realm" from libcurl
static auto ExtractRealm(ResponseHeaders const& rHeaders, char const* const pAuthHeaderName)
    -> ::std::optional<OUString>
{
    ::std::map<OUString, OUString> const headerMap(
        ProcessHeaders(rHeaders.HeaderFields.back().first));
    auto const it(headerMap.find(OUString::createFromAscii(pAuthHeaderName).toAsciiLowerCase()));
    if (it == headerMap.end())
    {
        SAL_WARN("ucb.ucp.webdav.curl", "cannot find auth header");
        return {};
    }
    // It may be possible that the header contains multiple methods each with
    // a different realm - extract only the first one bc the downstream API
    // only supports one anyway.
    // case insensitive!
    auto i(it->second.toAsciiLowerCase().indexOf("realm="));
    // is optional
    if (i == -1)
    {
        SAL_INFO("ucb.ucp.webdav.curl", "auth header has no realm");
        return {};
    }
    // no whitespace allowed before or after =
    i += ::std::strlen("realm=");
    if (it->second.getLength() < i + 2 || it->second[i] != '\"')
    {
        SAL_WARN("ucb.ucp.webdav.curl", "no realm value");
        return {};
    }
    ++i;
    OUStringBuffer buf;
    while (i < it->second.getLength() && it->second[i] != '\"')
    {
        if (it->second[i] == '\\') // quoted-pair escape
        {
            ++i;
            if (it->second.getLength() <= i)
            {
                SAL_WARN("ucb.ucp.webdav.curl", "unterminated quoted-pair");
                return {};
            }
        }
        buf.append(it->second[i]);
        ++i;
    }
    if (it->second.getLength() <= i)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "unterminated realm");
        return {};
    }
    return buf.makeStringAndClear();
}
 
CurlSession::CurlSession(uno::Reference<uno::XComponentContext> xContext,
                         ::rtl::Reference<DAVSessionFactory> const& rpFactory, OUString const& rURI,
                         uno::Sequence<beans::NamedValue> const& rFlags,
                         ::ucbhelper::InternetProxyDecider const& rProxyDecider)
    : DAVSession(rpFactory)
    , m_xContext(std::move(xContext))
    , m_Flags(rFlags)
    , m_URI(rURI)
    , m_Proxy(rProxyDecider.getProxy(m_URI.GetScheme(), m_URI.GetHost(), m_URI.GetPort()))
{
    assert(m_URI.GetScheme() == "http" || m_URI.GetScheme() == "https");
    m_pCurlMulti.reset(curl_multi_init());
    if (!m_pCurlMulti)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "curl_multi_init failed");
        throw DAVException(DAVException::DAV_SESSION_CREATE,
                           ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort()));
    }
    m_pCurl.reset(curl_easy_init());
    if (!m_pCurl)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "curl_easy_init failed");
        throw DAVException(DAVException::DAV_SESSION_CREATE,
                           ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort()));
    }
    m_ErrorBuffer[0] = '\0';
    // this supposedly gives the highest quality error reporting
    auto rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_ERRORBUFFER, m_ErrorBuffer);
    assert(rc == CURLE_OK);
#if 1
    // just for debugging...
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_DEBUGFUNCTION, debug_callback);
    assert(rc == CURLE_OK);
#endif
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_VERBOSE, 1L);
    assert(rc == CURLE_OK);
    // accept any encoding supported by libcurl
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_ACCEPT_ENCODING, "");
    if (rc != CURLE_OK)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_ACCEPT_ENCODING failed: " << GetErrorString(rc));
        throw DAVException(DAVException::DAV_SESSION_CREATE,
                           ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort()));
    }
    auto const connectTimeout(officecfg::Inet::Settings::ConnectTimeout::get());
    // default is 300s
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_CONNECTTIMEOUT,
                          ::std::max<long>(2L, ::std::min<long>(connectTimeout, 180L)));
    if (rc != CURLE_OK)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_CONNECTTIMEOUT failed: " << GetErrorString(rc));
        throw DAVException(DAVException::DAV_SESSION_CREATE,
                           ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort()));
    }
    auto const readTimeout(officecfg::Inet::Settings::ReadTimeout::get());
    m_nReadTimeout = ::std::max<int>(20, ::std::min<long>(readTimeout, 180)) * 1000;
    // default is infinite
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_TIMEOUT, 300L);
    if (rc != CURLE_OK)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_TIMEOUT failed: " << GetErrorString(rc));
        throw DAVException(DAVException::DAV_SESSION_CREATE,
                           ConnectionEndPointString(m_URI.GetHost(), m_URI.GetPort()));
    }
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_WRITEFUNCTION, &write_callback);
    assert(rc == CURLE_OK);
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_READFUNCTION, &read_callback);
    assert(rc == CURLE_OK);
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HEADERFUNCTION, &header_callback);
    assert(rc == CURLE_OK);
    ::InitCurl_easy(m_pCurl.get());
    if (officecfg::Office::Security::Net::AllowInsecureProtocols::get())
    {
    // tdf#149921 by default, with schannel (WNT) connection fails if revocation
    // lists cannot be checked; try to limit the checking to when revocation
    // lists can actually be retrieved (usually not the case for self-signed CA)
#if CURL_AT_LEAST_VERSION(7, 70, 0)
        rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_SSL_OPTIONS, CURLSSLOPT_REVOKE_BEST_EFFORT);
        assert(rc == CURLE_OK);
        rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_PROXY_SSL_OPTIONS,
                              CURLSSLOPT_REVOKE_BEST_EFFORT);
        assert(rc == CURLE_OK);
#endif
    }
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_SHARE, g_Init.pShare.get());
    assert(rc == CURLE_OK);
    // set this initially, may be overwritten during authentication
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_HTTPAUTH, CURLAUTH_ANY);
    assert(rc == CURLE_OK); // ANY is always available
    // always set CURLOPT_PROXY to suppress proxy detection in libcurl
    OString const utf8Proxy(OUStringToOString(m_Proxy, RTL_TEXTENCODING_UTF8));
    rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_PROXY, utf8Proxy.getStr());
    if (rc != CURLE_OK)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_PROXY failed: " << GetErrorString(rc));
        throw DAVException(DAVException::DAV_SESSION_CREATE, m_Proxy);
    }
    if (!m_Proxy.isEmpty())
    {
        // set this initially, may be overwritten during authentication
        rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_PROXYAUTH, CURLAUTH_ANY);
        assert(rc == CURLE_OK); // ANY is always available
    }
    auto const it(::std::find_if(m_Flags.begin(), m_Flags.end(),
                                 [](auto const& rFlag) { return rFlag.Name == "KeepAlive"; }));
    if (it != m_Flags.end() && it->Value.get<bool>())
    {
        // neon would close the connection from ne_end_request(), this seems
        // to be the equivalent and not CURLOPT_TCP_KEEPALIVE
        rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_FORBID_REUSE, 1L);
        assert(rc == CURLE_OK);
    }
    if (HostFilter::isExemptVerifyHost(m_URI.GetHost()))
    {
        rc = curl_easy_setopt(m_pCurl.get(), CURLOPT_SSL_VERIFYHOST, 0L);
        assert(rc == CURLE_OK);
    }
}
 
CurlSession::~CurlSession() {}
 
auto CurlSession::CanUse(OUString const& rURI, uno::Sequence<beans::NamedValue> const& rFlags)
    -> bool
{
    try
    {
        CurlUri const uri(rURI);
 
        return m_URI.GetScheme() == uri.GetScheme() && m_URI.GetHost() == uri.GetHost()
               && m_URI.GetPort() == uri.GetPort() && m_Flags == rFlags;
    }
    catch (DAVException const&)
    {
        return false;
    }
}
 
auto CurlSession::UsesProxy() -> bool
{
    assert(m_URI.GetScheme() == "http" || m_URI.GetScheme() == "https");
    return !m_Proxy.isEmpty();
}
 
auto CurlSession::abort() -> void
{
    // note: abort() was a no-op since OOo 3.2 and before that it crashed.
    bool expected(false);
    // it would be pointless to lock m_Mutex here as the other thread holds it
    if (m_AbortFlag.compare_exchange_strong(expected, true))
    {
        // This function looks safe to call without m_Mutex as long as the
        // m_pCurlMulti handle is not destroyed, and the caller must own a ref
        // to this object which keeps it alive; it should cause poll to return.
        curl_multi_wakeup(m_pCurlMulti.get());
    }
}
 
/// this is just a bunch of static member functions called from CurlSession
struct CurlProcessor
{
    static auto URIReferenceToURI(CurlSession& rSession, std::u16string_view rURIReference)
        -> CurlUri;
 
    static auto ProcessRequestImpl(
        CurlSession& rSession, CurlUri const& rURI, OUString const& rMethod,
        curl_slist* pRequestHeaderList, uno::Reference<io::XOutputStream> const* pxOutStream,
        uno::Sequence<sal_Int8> const* pInData,
        ::std::pair<::std::vector<OUString> const&, DAVResource&> const* pRequestedHeaders,
        ResponseHeaders& rHeaders) -> void;
 
    static auto ProcessRequest(
        CurlSession& rSession, CurlUri const& rURI, OUString const& rMethod,
        ::std::vector<CurlOption> const& rOptions, DAVRequestEnvironment const* pEnv,
        ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>>
            pRequestHeaderList,
        uno::Reference<io::XOutputStream> const* pxOutStream,
        uno::Reference<io::XInputStream> const* pxInStream,
        ::std::pair<::std::vector<OUString> const&, DAVResource&> const* pRequestedHeaders) -> void;
 
    static auto
    PropFind(CurlSession& rSession, CurlUri const& rURI, Depth depth,
             ::std::tuple<::std::vector<OUString> const&, ::std::vector<DAVResource>* const,
                          ::std::vector<ucb::Lock>* const> const* o_pRequestedProperties,
             ::std::vector<DAVResourceInfo>* const o_pResourceInfos,
             DAVRequestEnvironment const& rEnv) -> void;
 
    static auto MoveOrCopy(CurlSession& rSession, std::u16string_view rSourceURIReference,
                           ::std::u16string_view rDestinationURI, DAVRequestEnvironment const& rEnv,
                           bool isOverwrite, char const* pMethod) -> void;
 
    static auto Lock(CurlSession& rSession, CurlUri const& rURI, DAVRequestEnvironment const* pEnv,
                     ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>>
                         pRequestHeaderList,
                     uno::Reference<io::XInputStream> const* pxInStream)
        -> ::std::vector<::std::pair<ucb::Lock, sal_Int32>>;
 
    static auto Unlock(CurlSession& rSession, CurlUri const& rURI,
                       DAVRequestEnvironment const* pEnv) -> void;
};
 
auto CurlProcessor::URIReferenceToURI(CurlSession& rSession, std::u16string_view rURIReference)
    -> CurlUri
{
    // No need to acquire rSession.m_Mutex because accessed members are const.
    if (rSession.UsesProxy())
    // very odd, but see DAVResourceAccess::getRequestURI() :-/
    {
        assert(o3tl::starts_with(rURIReference, u"http://")
               || o3tl::starts_with(rURIReference, u"https://"));
        return CurlUri(rURIReference);
    }
    else
    {
        assert(o3tl::starts_with(rURIReference, u"/"));
        return rSession.m_URI.CloneWithRelativeRefPathAbsolute(rURIReference);
    }
}
 
/// main function to initiate libcurl requests
auto CurlProcessor::ProcessRequestImpl(
    CurlSession& rSession, CurlUri const& rURI, OUString const& rMethod,
    curl_slist* const pRequestHeaderList,
    uno::Reference<io::XOutputStream> const* const pxOutStream,
    uno::Sequence<sal_Int8> const* const pInData,
    ::std::pair<::std::vector<OUString> const&, DAVResource&> const* const pRequestedHeaders,
    ResponseHeaders& rHeaders) -> void
{
    ::comphelper::ScopeGuard const g([&]() {
        auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HEADERDATA, nullptr);
        assert(rc == CURLE_OK);
        (void)rc;
        if (pxOutStream)
        {
            rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_WRITEDATA, nullptr);
            assert(rc == CURLE_OK);
        }
        if (pInData)
        {
            rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_READDATA, nullptr);
            assert(rc == CURLE_OK);
            rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_UPLOAD, 0L);
            assert(rc == CURLE_OK);
        }
        if (pRequestHeaderList)
        {
            rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPHEADER, nullptr);
            assert(rc == CURLE_OK);
        }
    });
 
    if (pRequestHeaderList)
    {
        auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPHEADER, pRequestHeaderList);
        assert(rc == CURLE_OK);
        (void)rc;
    }
 
    auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_CURLU, rURI.GetCURLU());
    assert(rc == CURLE_OK); // can't fail since 7.63.0
 
    rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HEADERDATA, &rHeaders);
    assert(rc == CURLE_OK);
    ::std::optional<DownloadTarget> oDownloadTarget;
    if (pxOutStream)
    {
        oDownloadTarget.emplace(*pxOutStream, rHeaders);
        rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_WRITEDATA, &*oDownloadTarget);
        assert(rc == CURLE_OK);
    }
    ::std::optional<UploadSource> oUploadSource;
    if (pInData)
    {
        oUploadSource.emplace(*pInData);
        rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_READDATA, &*oUploadSource);
        assert(rc == CURLE_OK);
    }
    rSession.m_ErrorBuffer[0] = '\0';
 
    // note: easy handle must be added for *every* transfer!
    // otherwise it gets stuck in MSTATE_MSGSENT forever after 1st transfer
    auto mc = curl_multi_add_handle(rSession.m_pCurlMulti.get(), rSession.m_pCurl.get());
    if (mc != CURLM_OK)
    {
        SAL_WARN("ucb.ucp.webdav.curl",
                 "curl_multi_add_handle failed: " << GetErrorStringMulti(mc));
        throw DAVException(
            DAVException::DAV_SESSION_CREATE,
            ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()));
    }
    ::comphelper::ScopeGuard const gg([&]() {
        mc = curl_multi_remove_handle(rSession.m_pCurlMulti.get(), rSession.m_pCurl.get());
        if (mc != CURLM_OK)
        {
            SAL_WARN("ucb.ucp.webdav.curl",
                     "curl_multi_remove_handle failed: " << GetErrorStringMulti(mc));
        }
    });
 
    // this is where libcurl actually does something
    rc = CURL_LAST; // clear current value
    int nRunning;
    do
    {
        mc = curl_multi_perform(rSession.m_pCurlMulti.get(), &nRunning);
        if (mc != CURLM_OK)
        {
            SAL_WARN("ucb.ucp.webdav.curl",
                     "curl_multi_perform failed: " << GetErrorStringMulti(mc));
            throw DAVException(
                DAVException::DAV_HTTP_CONNECT,
                ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()));
        }
        if (nRunning == 0)
        { // short request like HEAD on loopback could be done in first call
            break;
        }
        int nFDs;
        mc = curl_multi_poll(rSession.m_pCurlMulti.get(), nullptr, 0, rSession.m_nReadTimeout,
                             &nFDs);
        if (mc != CURLM_OK)
        {
            SAL_WARN("ucb.ucp.webdav.curl", "curl_multi_poll failed: " << GetErrorStringMulti(mc));
            throw DAVException(
                DAVException::DAV_HTTP_CONNECT,
                ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()));
        }
        if (rSession.m_AbortFlag.load())
        { // flag was set by abort() -> not sure what exception to throw?
            throw DAVException(DAVException::DAV_HTTP_ERROR, u"abort() was called"_ustr, 0);
        }
    } while (nRunning != 0);
    // there should be exactly 1 CURLMsg now, but the interface is
    // extensible so future libcurl versions could yield additional things
    do
    {
        CURLMsg const* const pMsg = curl_multi_info_read(rSession.m_pCurlMulti.get(), &nRunning);
        if (pMsg && pMsg->msg == CURLMSG_DONE)
        {
            assert(pMsg->easy_handle == rSession.m_pCurl.get());
            rc = pMsg->data.result;
        }
        else
        {
            SAL_WARN("ucb.ucp.webdav.curl", "curl_multi_info_read unexpected result");
        }
    } while (nRunning != 0);
 
    // error handling part 1: libcurl errors
    if (rc != CURLE_OK)
    {
        // TODO: is there any value in extracting CURLINFO_OS_ERRNO
        auto const errorString(GetErrorString(rc, rSession.m_ErrorBuffer));
        SAL_WARN("ucb.ucp.webdav.curl", "curl_easy_perform failed: " << errorString);
        switch (rc)
        {
            case CURLE_UNSUPPORTED_PROTOCOL:
                throw DAVException(DAVException::DAV_UNSUPPORTED, u""_ustr,
                                   rtl::OStringToOUString(errorString, RTL_TEXTENCODING_UTF8));
            case CURLE_COULDNT_RESOLVE_PROXY:
                throw DAVException(DAVException::DAV_HTTP_LOOKUP, rSession.m_Proxy,
                                   rtl::OStringToOUString(errorString, RTL_TEXTENCODING_UTF8));
            case CURLE_COULDNT_RESOLVE_HOST:
                throw DAVException(
                    DAVException::DAV_HTTP_LOOKUP,
                    ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()),
                    rtl::OStringToOUString(errorString, RTL_TEXTENCODING_UTF8));
            case CURLE_COULDNT_CONNECT:
            case CURLE_SSL_CONNECT_ERROR:
            case CURLE_SSL_CERTPROBLEM:
            case CURLE_SSL_CIPHER:
            case CURLE_PEER_FAILED_VERIFICATION:
            case CURLE_SSL_ISSUER_ERROR:
            case CURLE_SSL_PINNEDPUBKEYNOTMATCH:
            case CURLE_SSL_INVALIDCERTSTATUS:
            case CURLE_FAILED_INIT:
#if CURL_AT_LEAST_VERSION(7, 69, 0)
            case CURLE_QUIC_CONNECT_ERROR:
#endif
                throw DAVException(
                    DAVException::DAV_HTTP_CONNECT,
                    ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()),
                    rtl::OStringToOUString(errorString, RTL_TEXTENCODING_UTF8));
            case CURLE_REMOTE_ACCESS_DENIED:
            case CURLE_LOGIN_DENIED:
            case CURLE_AUTH_ERROR:
                throw DAVException(
                    DAVException::DAV_HTTP_AUTH, // probably?
                    ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()),
                    rtl::OStringToOUString(errorString, RTL_TEXTENCODING_UTF8));
            case CURLE_WRITE_ERROR:
            case CURLE_READ_ERROR: // error returned from our callbacks
            case CURLE_OUT_OF_MEMORY:
            case CURLE_ABORTED_BY_CALLBACK:
            case CURLE_BAD_FUNCTION_ARGUMENT:
            case CURLE_SEND_ERROR:
            case CURLE_RECV_ERROR:
            case CURLE_SSL_CACERT_BADFILE:
            case CURLE_SSL_CRL_BADFILE:
            case CURLE_RECURSIVE_API_CALL:
                throw DAVException(
                    DAVException::DAV_HTTP_FAILED,
                    ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()),
                    rtl::OStringToOUString(errorString, RTL_TEXTENCODING_UTF8));
            case CURLE_OPERATION_TIMEDOUT:
                throw DAVException(
                    DAVException::DAV_HTTP_TIMEOUT,
                    ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()),
                    rtl::OStringToOUString(errorString, RTL_TEXTENCODING_UTF8));
            default: // lots of generic errors
                throw DAVException(DAVException::DAV_HTTP_ERROR, u""_ustr,
                                   rtl::OStringToOUString(errorString, RTL_TEXTENCODING_UTF8));
        }
    }
    // error handling part 2: HTTP status codes
    long statusCode(SC_NONE);
    rc = curl_easy_getinfo(rSession.m_pCurl.get(), CURLINFO_RESPONSE_CODE, &statusCode);
    assert(rc == CURLE_OK);
    assert(statusCode != SC_NONE); // ??? should be error returned from perform?
    SAL_INFO("ucb.ucp.webdav.curl", "HTTP status code: " << statusCode);
    if (statusCode < 300)
    {
        // neon did this regardless of status or even error, which seems odd
        ExtractRequestedHeaders(rHeaders, pRequestedHeaders);
    }
    else
    {
        // create message containing the HTTP method and response status line
        OUString statusLine("\n" + rMethod + "\n=>\n");
        if (!rHeaders.HeaderFields.empty() && !rHeaders.HeaderFields.back().first.empty()
            && rHeaders.HeaderFields.back().first.front().startsWith("HTTP"))
        {
            statusLine += ::rtl::OStringToOUString(
                ::o3tl::trim(rHeaders.HeaderFields.back().first.front()),
                RTL_TEXTENCODING_ASCII_US);
        }
        switch (statusCode)
        {
            case SC_REQUEST_TIMEOUT:
            {
                throw DAVException(
                    DAVException::DAV_HTTP_TIMEOUT,
                    ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()));
                break;
            }
            case SC_MOVED_PERMANENTLY:
            case SC_MOVED_TEMPORARILY:
            case SC_SEE_OTHER:
            case SC_TEMPORARY_REDIRECT:
            {
                // could also use CURLOPT_FOLLOWLOCATION but apparently the
                // upper layer wants to know about redirects?
                char* pRedirectURL(nullptr);
                rc = curl_easy_getinfo(rSession.m_pCurl.get(), CURLINFO_REDIRECT_URL,
                                       &pRedirectURL);
                assert(rc == CURLE_OK);
                if (pRedirectURL)
                {
                    // Sharepoint 2016 workaround: contains unencoded U+0020
                    OUString const redirectURL(::rtl::Uri::encode(
                        OUString(pRedirectURL, strlen(pRedirectURL), RTL_TEXTENCODING_UTF8),
                        rtl_UriCharClassUric, rtl_UriEncodeKeepEscapes, RTL_TEXTENCODING_UTF8));
 
                    throw DAVException(DAVException::DAV_HTTP_REDIRECT, redirectURL);
                }
                [[fallthrough]];
            }
            default:
                throw DAVException(DAVException::DAV_HTTP_ERROR, statusLine, statusCode);
        }
    }
 
    if (pxOutStream)
    {
        (*pxOutStream)->closeOutput(); // signal EOF
    }
}
 
static auto TryRemoveExpiredLockToken(CurlSession& rSession, CurlUri const& rURI,
                                      DAVRequestEnvironment const* const pEnv) -> bool
{
    if (!pEnv)
    {
        // caller was a NonInteractive_*LOCK function anyway, its caller is LockStore
        return false;
    }
    OUString const* const pToken(g_Init.LockStore.getLockTokenForURI(rURI.GetURI(), nullptr));
    if (!pToken)
    {
        return false;
    }
    try
    {
        // determine validity of existing lock via lockdiscovery request
        ::std::vector<OUString> const propertyNames{ DAVProperties::LOCKDISCOVERY };
        ::std::vector<ucb::Lock> locks;
        ::std::tuple<::std::vector<OUString> const&, ::std::vector<DAVResource>* const,
                     ::std::vector<ucb::Lock>* const> const args(propertyNames, nullptr, &locks);
 
        CurlProcessor::PropFind(rSession, rURI, DAVZERO, &args, nullptr, *pEnv);
 
        // https://datatracker.ietf.org/doc/html/rfc4918#section-15.8
        // The response MAY not contain tokens, but hopefully it
        // will if client is properly authenticated.
        if (::std::any_of(locks.begin(), locks.end(), [pToken](ucb::Lock const& rLock) {
                return ::std::any_of(
                    rLock.LockTokens.begin(), rLock.LockTokens.end(),
                    [pToken](OUString const& rToken) { return *pToken == rToken; });
            }))
        {
            return false; // still have the lock
        }
 
        SAL_INFO("ucb.ucp.webdav.curl",
                 "lock token expired, removing: " << rURI.GetURI() << " " << *pToken);
        g_Init.LockStore.removeLock(rURI.GetURI());
        return true;
    }
    catch (DAVException const&)
    {
        return false; // ignore, the caller already has a better exception
    }
}
 
auto CurlProcessor::ProcessRequest(
    CurlSession& rSession, CurlUri const& rURI, OUString const& rMethod,
    ::std::vector<CurlOption> const& rOptions, DAVRequestEnvironment const* const pEnv,
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>>
        pRequestHeaderList,
    uno::Reference<io::XOutputStream> const* const pxOutStream,
    uno::Reference<io::XInputStream> const* const pxInStream,
    ::std::pair<::std::vector<OUString> const&, DAVResource&> const* const pRequestedHeaders)
    -> void
{
    if (HostFilter::isForbidden(rURI.GetHost()))
    {
        SAL_WARN("ucb.ucp.webdav.curl", "Access denied to host: " << rURI.GetHost());
        throw uno::RuntimeException(u"access to host denied"_ustr);
    }
 
    if (pEnv)
    { // add custom request headers passed by caller
        for (auto const& rHeader : pEnv->m_aRequestHeaders)
        {
            OString const utf8Header(
                OUStringToOString(rHeader.first, RTL_TEXTENCODING_ASCII_US) + ": "
                + OUStringToOString(rHeader.second, RTL_TEXTENCODING_ASCII_US));
            pRequestHeaderList.reset(
                curl_slist_append(pRequestHeaderList.release(), utf8Header.getStr()));
            if (!pRequestHeaderList)
            {
                throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
            }
        }
    }
 
    uno::Sequence<sal_Int8> data;
    if (pxInStream)
    {
        uno::Reference<io::XSeekable> const xSeekable(*pxInStream, uno::UNO_QUERY);
        if (xSeekable.is())
        {
            auto const len(xSeekable->getLength() - xSeekable->getPosition());
            if ((**pxInStream).readBytes(data, len) != len)
            {
                throw uno::RuntimeException(u"short readBytes"_ustr);
            }
        }
        else
        {
            ::std::vector<uno::Sequence<sal_Int8>> bufs;
            bool isDone(false);
            do
            {
                bufs.emplace_back();
                isDone = (**pxInStream).readSomeBytes(bufs.back(), 65536) == 0;
            } while (!isDone);
            sal_Int32 nSize(0);
            for (auto const& rBuf : bufs)
            {
                if (o3tl::checked_add(nSize, rBuf.getLength(), nSize))
                {
                    throw std::bad_alloc(); // too large for Sequence
                }
            }
            data.realloc(nSize);
            size_t nCopied(0);
            for (auto const& rBuf : bufs)
            {
                ::std::memcpy(data.getArray() + nCopied, rBuf.getConstArray(), rBuf.getLength());
                nCopied += rBuf.getLength(); // can't overflow
            }
        }
    }
 
    // Clear flag before transfer starts; only a transfer started before
    // calling abort() will be aborted, not one started later.
    rSession.m_AbortFlag.store(false);
 
    Guard guard(rSession.m_Mutex, rOptions, rURI, rSession.m_pCurl.get());
 
    // authentication data may be in the URI, or requested via XInteractionHandler
    struct Auth
    {
        OUString UserName;
        OUString PassWord;
        decltype(CURLAUTH_ANY) AuthMask; ///< allowed auth methods
        Auth(OUString aUserName, OUString aPassword, decltype(CURLAUTH_ANY) aAuthMask)
            : UserName(std::move(aUserName))
            , PassWord(std::move(aPassword))
            , AuthMask(aAuthMask)
        {
        }
    };
    ::std::optional<Auth> oAuth;
    ::std::optional<Auth> oAuthProxy;
    if (pEnv && !rSession.m_isAuthenticatedProxy && !rSession.m_Proxy.isEmpty())
    {
        try
        {
            // the hope is that this must be a URI
            CurlUri const uri(rSession.m_Proxy);
            if (!uri.GetUser().isEmpty() || !uri.GetPassword().isEmpty())
            {
                oAuthProxy.emplace(uri.GetUser(), uri.GetPassword(), CURLAUTH_ANY);
            }
        }
        catch (DAVException&)
        {
            // ignore any parsing failure here
        }
    }
    decltype(CURLAUTH_ANY) const authSystem(CURLAUTH_NEGOTIATE | CURLAUTH_NTLM | CURLAUTH_NTLM_WB);
    if (pRequestedHeaders || (pEnv && !rSession.m_isAuthenticated))
    {
    // m_aRequestURI *may* be a path or *may* be URI - wtf
    // TODO: why is there this m_aRequestURI and also rURIReference argument?
    // ... only caller is DAVResourceAccess - always identical except MOVE/COPY
    // which doesn't work if it's just a URI reference so let's just use
    // rURIReference via rURI instead
#if 0
        CurlUri const uri(pEnv->m_aRequestURI);
#endif
        // note: due to parsing bug pwd didn't work in previous webdav ucps
        if (pEnv && !rSession.m_isAuthenticated
            && (!rURI.GetUser().isEmpty() || !rURI.GetPassword().isEmpty()))
        {
            oAuth.emplace(rURI.GetUser(), rURI.GetPassword(), CURLAUTH_ANY);
        }
        if (pRequestedHeaders)
        {
            // note: Previously this would be the rURIReference directly but
            // that ends up in CurlUri anyway and curl is unhappy.
            // But it looks like all consumers of this .uri are interested
            // only in the path, so it shouldn't make a difference to give
            // the entire URI when the caller extracts the path anyway.
            pRequestedHeaders->second.uri = rURI.GetURI();
            pRequestedHeaders->second.properties.clear();
        }
    }
    bool isRetry(false);
    bool isFallbackHTTP10(false);
    int nAuthRequests(0);
    int nAuthRequestsProxy(0);
 
    // libcurl does not have an authentication callback so handle auth
    // related status codes and requesting credentials via this loop
    do
    {
        isRetry = false;
 
        // re-check m_isAuthenticated flags every time, could have been set
        // by re-entrant call
        if (oAuth && !rSession.m_isAuthenticated)
        {
            OString const utf8UserName(OUStringToOString(oAuth->UserName, RTL_TEXTENCODING_UTF8));
            auto rc
                = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_USERNAME, utf8UserName.getStr());
            if (rc != CURLE_OK)
            {
                SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_USERNAME failed: " << GetErrorString(rc));
                throw DAVException(DAVException::DAV_INVALID_ARG);
            }
            OString const utf8PassWord(OUStringToOString(oAuth->PassWord, RTL_TEXTENCODING_UTF8));
            rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_PASSWORD, utf8PassWord.getStr());
            if (rc != CURLE_OK)
            {
                SAL_WARN("ucb.ucp.webdav.curl", "CURLOPT_PASSWORD failed: " << GetErrorString(rc));
                throw DAVException(DAVException::DAV_INVALID_ARG);
            }
            rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPAUTH, oAuth->AuthMask);
            if (rc != CURLE_OK)
            { // NEGOTIATE typically disabled on Linux, NTLM is optional too
                assert(rc == CURLE_NOT_BUILT_IN);
                SAL_INFO("ucb.ucp.webdav.curl", "no auth method available");
                throw DAVException(
                    DAVException::DAV_HTTP_NOAUTH,
                    ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()));
            }
        }
 
        if (oAuthProxy && !rSession.m_isAuthenticatedProxy)
        {
            OString const utf8UserName(
                OUStringToOString(oAuthProxy->UserName, RTL_TEXTENCODING_UTF8));
            auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_PROXYUSERNAME,
                                       utf8UserName.getStr());
            if (rc != CURLE_OK)
            {
                SAL_WARN("ucb.ucp.webdav.curl",
                         "CURLOPT_PROXYUSERNAME failed: " << GetErrorString(rc));
                throw DAVException(DAVException::DAV_INVALID_ARG);
            }
            OString const utf8PassWord(
                OUStringToOString(oAuthProxy->PassWord, RTL_TEXTENCODING_UTF8));
            rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_PROXYPASSWORD,
                                  utf8PassWord.getStr());
            if (rc != CURLE_OK)
            {
                SAL_WARN("ucb.ucp.webdav.curl",
                         "CURLOPT_PROXYPASSWORD failed: " << GetErrorString(rc));
                throw DAVException(DAVException::DAV_INVALID_ARG);
            }
            rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_PROXYAUTH, oAuthProxy->AuthMask);
            if (rc != CURLE_OK)
            { // NEGOTIATE typically disabled on Linux, NTLM is optional too
                assert(rc == CURLE_NOT_BUILT_IN);
                SAL_INFO("ucb.ucp.webdav.curl", "no auth method available");
                throw DAVException(
                    DAVException::DAV_HTTP_NOAUTH,
                    ConnectionEndPointString(rSession.m_URI.GetHost(), rSession.m_URI.GetPort()));
            }
        }
 
        ResponseHeaders headers(rSession.m_pCurl.get());
        // always pass a stream for debug logging, buffer the result body
        uno::Reference<io::XSequenceOutputStream> const xSeqOutStream(
            io::SequenceOutputStream::create(rSession.m_xContext));
        uno::Reference<io::XOutputStream> const xTempOutStream(xSeqOutStream);
        assert(xTempOutStream.is());
 
        try
        {
            ProcessRequestImpl(rSession, rURI, rMethod, pRequestHeaderList.get(), &xTempOutStream,
                               pxInStream ? &data : nullptr, pRequestedHeaders, headers);
            if (pxOutStream)
            { // only copy to result stream if transfer was successful
                (*pxOutStream)->writeBytes(xSeqOutStream->getWrittenBytes());
                (*pxOutStream)->closeOutput(); // signal EOF
            }
        }
        catch (DAVException const& rException)
        {
            // log start of request body if there was any
            auto const bytes(xSeqOutStream->getWrittenBytes());
            auto const len(::std::min<sal_Int32>(bytes.getLength(), 10000));
            SAL_INFO("ucb.ucp.webdav.curl",
                     "DAVException; (first) " << len << " bytes of data received:");
            if (0 < len)
            {
                OStringBuffer buf(len);
                for (sal_Int32 i = 0; i < len; ++i)
                {
                    if (bytes[i] < 0x20) // also if negative
                    {
                        static char const hexDigit[16] = { '0', '1', '2', '3', '4', '5', '6', '7',
                                                           '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
                        buf.append(OString::Concat("\\x")
                                   + OStringChar(hexDigit[static_cast<sal_uInt8>(bytes[i]) >> 4])
                                   + OStringChar(hexDigit[bytes[i] & 0x0F]));
                    }
                    else
                    {
                        buf.append(static_cast<char>(bytes[i]));
                    }
                }
                SAL_INFO("ucb.ucp.webdav.curl", buf.makeStringAndClear());
            }
 
            // error handling part 3: special HTTP status codes
            // that require unlocking m_Mutex to handle
            if (rException.getError() == DAVException::DAV_HTTP_ERROR)
            {
                auto const statusCode(rException.getStatus());
                switch (statusCode)
                {
                    case SC_LOCKED:
                    {
                        guard.Release(); // release m_Mutex before accessing LockStore
                        if (g_Init.LockStore.getLockTokenForURI(rURI.GetURI(), nullptr))
                        {
                            throw DAVException(DAVException::DAV_LOCKED_SELF);
                        }
                        else // locked by third party
                        {
                            throw DAVException(DAVException::DAV_LOCKED);
                        }
                        break;
                    }
                    case SC_PRECONDITION_FAILED:
                    case SC_BAD_REQUEST:
                    {
                        guard.Release(); // release m_Mutex before accessing LockStore
                        // Not obvious but apparently these codes may indicate
                        // the expiration of a lock.
                        // Initiate a new request *outside* ProcessRequestImpl
                        // *after* rGuard.unlock() to avoid messing up m_pCurl state.
                        if (TryRemoveExpiredLockToken(rSession, rURI, pEnv))
                        {
                            throw DAVException(DAVException::DAV_LOCK_EXPIRED);
                        }
                        break;
                    }
                    case SC_FORBIDDEN:
                    {
                        ::std::map<OUString, OUString> const headerMap(
                            ProcessHeaders(headers.HeaderFields.back().first));
                        // X-MSDAVEXT_Error see [MS-WEBDAVE] 2.2.3.1.9
                        auto const it(headerMap.find(u"x-msdavext_error"_ustr));
                        if (it == headerMap.end() || !it->second.startsWith("917656;"))
                        {
                            break;
                        }
                        // fallback needs cookie engine enabled
                        CURLcode rc
                            = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_COOKIEFILE, "");
                        assert(rc == CURLE_OK);
                        (void)rc;
                        SAL_INFO("ucb.ucp.webdav.curl", "403 fallback authentication");
                        // disable 302 redirect
                        pRequestHeaderList.reset(curl_slist_append(
                            pRequestHeaderList.release(), "X-FORMS_BASED_AUTH_ACCEPTED: f"));
                        if (!pRequestHeaderList)
                        {
                            throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
                        }
                    }
                        [[fallthrough]]; // SP, no cookie, or cookie failed: try NTLM/Negotiate
                    case SC_UNAUTHORIZED:
                    case SC_PROXY_AUTHENTICATION_REQUIRED:
                    {
                        (statusCode != SC_PROXY_AUTHENTICATION_REQUIRED
                             ? rSession.m_isAuthenticated
                             : rSession.m_isAuthenticatedProxy)
                            = false; // any auth data in m_pCurl is invalid
                        auto& rnAuthRequests(statusCode != SC_PROXY_AUTHENTICATION_REQUIRED
                                                 ? nAuthRequests
                                                 : nAuthRequestsProxy);
                        if (rnAuthRequests == 10)
                        {
                            SAL_INFO("ucb.ucp.webdav.curl", "aborting authentication after "
                                                                << rnAuthRequests << " attempts");
                        }
                        else if (pEnv && pEnv->m_xAuthListener)
                        {
                            ::std::optional<OUString> const oRealm(
                                ExtractRealm(headers, statusCode != SC_PROXY_AUTHENTICATION_REQUIRED
                                                          ? "WWW-Authenticate"
                                                          : "Proxy-Authenticate"));
 
                            ::std::optional<Auth>& roAuth(
                                statusCode != SC_PROXY_AUTHENTICATION_REQUIRED ? oAuth
                                                                               : oAuthProxy);
                            OUString userName(roAuth ? roAuth->UserName : OUString());
                            OUString passWord(roAuth ? roAuth->PassWord : OUString());
                            long authAvail(0);
                            auto rc
                                = curl_easy_getinfo(rSession.m_pCurl.get(),
                                                    statusCode != SC_PROXY_AUTHENTICATION_REQUIRED
                                                        ? CURLINFO_HTTPAUTH_AVAIL
                                                        : CURLINFO_PROXYAUTH_AVAIL,
                                                    &authAvail);
                            assert(rc == CURLE_OK);
                            if (statusCode == SC_FORBIDDEN)
                            { // SharePoint fallback: try Negotiate auth
                                assert(authAvail == 0);
                                // note: this must be a single value!
                                // would need 2 iterations to try CURLAUTH_NTLM too
                                rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPAUTH,
                                                      CURLAUTH_NEGOTIATE);
                                if (rc == CURLE_OK)
                                {
                                    authAvail = CURLAUTH_NEGOTIATE;
                                }
                                else
                                {
                                    rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTPAUTH,
                                                          CURLAUTH_NTLM);
                                    if (rc == CURLE_OK)
                                    {
                                        authAvail = CURLAUTH_NTLM;
                                    }
                                    else
                                    { // can't work
                                        SAL_INFO("ucb.ucp.webdav.curl",
                                                 "no SP fallback auth method available");
                                        throw DAVException(
                                            DAVException::DAV_HTTP_NOAUTH,
                                            ConnectionEndPointString(rSession.m_URI.GetHost(),
                                                                     rSession.m_URI.GetPort()));
                                    }
                                }
                            }
                            // only allow SystemCredentials once - the
                            // PasswordContainer may have stored it in the
                            // Config (TrySystemCredentialsFirst or
                            // AuthenticateUsingSystemCredentials) and then it
                            // will always force its use no matter how hopeless
                            bool const isSystemCredSupported((authAvail & authSystem) != 0
                                                             && rnAuthRequests == 0);
                            ++rnAuthRequests;
 
                            // Ask user via XInteractionHandler.
                            // Warning: This likely runs an event loop which may
                            // end up calling back into this instance, so all
                            // changes to m_pCurl must be undone now and
                            // restored after return.
                            guard.Release();
 
                            auto const ret = pEnv->m_xAuthListener->authenticate(
                                oRealm ? *oRealm : u""_ustr,
                                statusCode != SC_PROXY_AUTHENTICATION_REQUIRED
                                    ? rSession.m_URI.GetHost()
                                    : rSession.m_Proxy,
                                userName, passWord, isSystemCredSupported);
 
                            if (ret == 0)
                            {
                                // NTLM may either use a password requested
                                // from the user, or from the system via SSPI
                                // so i guess it should not be disabled here
                                // regardless of the state of the system auth
                                // checkbox, particularly since SSPI is only
                                // available on WNT.
                                // Additionally, "Negotiate" has a "legacy"
                                // mode that is actually just NTLM according to
                                // https://curl.se/rfc/ntlm.html#ntlmHttpAuthentication
                                // so there's nothing in authSystem that can be
                                // disabled here.
                                roAuth.emplace(userName, passWord,
                                               ((userName.isEmpty() && passWord.isEmpty())
                                                    ? (authAvail & authSystem)
                                                    : authAvail));
                                isRetry = true;
                                // Acquire is only necessary in case of success.
                                guard.Acquire();
                                break; // break out of switch
                            }
                            // else: throw
                        }
                        SAL_INFO("ucb.ucp.webdav.curl", "no auth credentials provided");
                        throw DAVException(DAVException::DAV_HTTP_NOAUTH,
                                           ConnectionEndPointString(rSession.m_URI.GetHost(),
                                                                    rSession.m_URI.GetPort()));
                        break;
                    }
                }
            }
            else if (rException.getError() == DAVException::DAV_UNSUPPORTED)
            {
                // tdf#152493 libcurl can't handle "Transfer-Encoding: chunked"
                // in HTTP/1.1 100 Continue response.
                // workaround: if HTTP/1.1 didn't work, try HTTP/1.0
                // (but fallback only once - to prevent infinite loop)
                if (isFallbackHTTP10)
                {
                    throw DAVException(DAVException::DAV_HTTP_ERROR);
                }
                isFallbackHTTP10 = true;
                // note: this is not reset - future requests to this URI use it!
                auto rc = curl_easy_setopt(rSession.m_pCurl.get(), CURLOPT_HTTP_VERSION,
                                           CURL_HTTP_VERSION_1_0);
                if (rc != CURLE_OK)
                {
                    throw DAVException(DAVException::DAV_HTTP_ERROR);
                }
                SAL_INFO("ucb.ucp.webdav.curl", "attempting fallback to HTTP/1.0");
                isRetry = true;
            }
            if (!isRetry)
            {
                throw; // everything else: re-throw
            }
        }
    } while (isRetry);
 
    if (oAuth)
    {
        // assume this worked, leave auth data as stored in m_pCurl
        rSession.m_isAuthenticated = true;
    }
    if (oAuthProxy)
    {
        // assume this worked, leave auth data as stored in m_pCurl
        rSession.m_isAuthenticatedProxy = true;
    }
}
 
auto CurlSession::OPTIONS(OUString const& rURIReference,
 
                          DAVOptions& rOptions, DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "OPTIONS: " << rURIReference);
 
    rOptions.init();
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    ::std::vector<OUString> const headerNames{ u"allow"_ustr, u"dav"_ustr };
    DAVResource result;
    ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(headerNames, result);
 
    ::std::vector<CurlOption> const options{ { CURLOPT_CUSTOMREQUEST, "OPTIONS",
                                               "CURLOPT_CUSTOMREQUEST" } };
 
    CurlProcessor::ProcessRequest(*this, uri, u"OPTIONS"_ustr, options, &rEnv, nullptr, nullptr,
                                  nullptr, &headers);
 
    for (auto const& it : result.properties)
    {
        OUString value;
        it.Value >>= value;
        SAL_INFO("ucb.ucp.webdav.curl", "OPTIONS: header: " << it.Name << ": " << value);
        if (it.Name.equalsIgnoreAsciiCase("allow"))
        {
            rOptions.setAllowedMethods(value);
        }
        else if (it.Name.equalsIgnoreAsciiCase("dav"))
        {
            // see <http://tools.ietf.org/html/rfc4918#section-10.1>,
            // <http://tools.ietf.org/html/rfc4918#section-18>,
            // and <http://tools.ietf.org/html/rfc7230#section-3.2>
            // we detect the class (1, 2 and 3), other elements (token, URL)
            // are not used for now
            auto const list(::comphelper::string::convertCommaSeparated(value));
            for (OUString const& v : list)
            {
                if (v == "1")
                {
                    rOptions.setClass1();
                }
                else if (v == "2")
                {
                    rOptions.setClass2();
                }
                else if (v == "3")
                {
                    rOptions.setClass3();
                }
            }
        }
    }
    if (rOptions.isClass2() || rOptions.isClass3())
    {
        if (g_Init.LockStore.getLockTokenForURI(uri.GetURI(), nullptr))
        {
            rOptions.setLocked();
        }
    }
}
 
auto CurlProcessor::PropFind(
    CurlSession& rSession, CurlUri const& rURI, Depth const nDepth,
    ::std::tuple<::std::vector<OUString> const&, ::std::vector<DAVResource>* const,
                 ::std::vector<ucb::Lock>* const> const* const o_pRequestedProperties,
    ::std::vector<DAVResourceInfo>* const o_pResourceInfos, DAVRequestEnvironment const& rEnv)
    -> void
{
    assert((o_pRequestedProperties != nullptr) != (o_pResourceInfos != nullptr));
    assert((o_pRequestedProperties == nullptr)
           || (::std::get<1>(*o_pRequestedProperties) != nullptr)
                  != (::std::get<2>(*o_pRequestedProperties) != nullptr));
 
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList;
    pList.reset(curl_slist_append(pList.release(), "Content-Type: application/xml"));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
    OString depth;
    switch (nDepth)
    {
        case DAVZERO:
            depth = "Depth: 0"_ostr;
            break;
        case DAVONE:
            depth = "Depth: 1"_ostr;
            break;
        case DAVINFINITY:
            depth = "Depth: infinity"_ostr;
            break;
        default:
            assert(false);
    }
    pList.reset(curl_slist_append(pList.release(), depth.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
 
    uno::Reference<io::XSequenceOutputStream> const xSeqOutStream(
        io::SequenceOutputStream::create(rSession.m_xContext));
    uno::Reference<io::XOutputStream> const xRequestOutStream(xSeqOutStream);
    assert(xRequestOutStream.is());
 
    uno::Reference<xml::sax::XWriter> const xWriter(xml::sax::Writer::create(rSession.m_xContext));
    xWriter->setOutputStream(xRequestOutStream);
    xWriter->startDocument();
    rtl::Reference<::comphelper::AttributeList> const pAttrList(new ::comphelper::AttributeList);
    pAttrList->AddAttribute(u"xmlns"_ustr, u"DAV:"_ustr);
    xWriter->startElement(u"propfind"_ustr, pAttrList);
    if (o_pResourceInfos)
    {
        xWriter->startElement(u"propname"_ustr, nullptr);
        xWriter->endElement(u"propname"_ustr);
    }
    else
    {
        if (::std::get<0>(*o_pRequestedProperties).empty())
        {
            xWriter->startElement(u"allprop"_ustr, nullptr);
            xWriter->endElement(u"allprop"_ustr);
        }
        else
        {
            xWriter->startElement(u"prop"_ustr, nullptr);
            for (OUString const& rName : ::std::get<0>(*o_pRequestedProperties))
            {
                SerfPropName name;
                DAVProperties::createSerfPropName(rName, name);
                pAttrList->Clear();
                pAttrList->AddAttribute(u"xmlns"_ustr, OUString::createFromAscii(name.nspace));
                xWriter->startElement(OUString::createFromAscii(name.name), pAttrList);
                xWriter->endElement(OUString::createFromAscii(name.name));
            }
            xWriter->endElement(u"prop"_ustr);
        }
    }
    xWriter->endElement(u"propfind"_ustr);
    xWriter->endDocument();
 
    uno::Reference<io::XInputStream> const xRequestInStream(
        io::SequenceInputStream::createStreamFromSequence(rSession.m_xContext,
                                                          xSeqOutStream->getWrittenBytes()));
    assert(xRequestInStream.is());
 
    curl_off_t const len(xSeqOutStream->getWrittenBytes().getLength());
 
    ::std::vector<CurlOption> const options{
        { CURLOPT_UPLOAD, 1L, nullptr },
        { CURLOPT_CUSTOMREQUEST, "PROPFIND", "CURLOPT_CUSTOMREQUEST" },
        // note: Sharepoint cannot handle "Transfer-Encoding: chunked"
        { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT }
    };
 
    // stream for response
    uno::Reference<io::XInputStream> const xResponseInStream(io::Pipe::create(rSession.m_xContext));
    uno::Reference<io::XOutputStream> const xResponseOutStream(xResponseInStream, uno::UNO_QUERY);
    assert(xResponseInStream.is());
    assert(xResponseOutStream.is());
 
    CurlProcessor::ProcessRequest(rSession, rURI, u"PROPFIND"_ustr, options, &rEnv,
                                  ::std::move(pList), &xResponseOutStream, &xRequestInStream,
                                  nullptr);
 
    if (o_pResourceInfos)
    {
        *o_pResourceInfos = parseWebDAVPropNameResponse(xResponseInStream);
    }
    else
    {
        if (::std::get<1>(*o_pRequestedProperties) != nullptr)
        {
            *::std::get<1>(*o_pRequestedProperties)
                = parseWebDAVPropFindResponse(xResponseInStream);
            for (DAVResource& it : *::std::get<1>(*o_pRequestedProperties))
            {
                // caller will give these uris to CurlUri so can't be relative
                if (it.uri.startsWith("/"))
                {
                    try
                    {
                        it.uri = rSession.m_URI.CloneWithRelativeRefPathAbsolute(it.uri).GetURI();
                    }
                    catch (DAVException const&)
                    {
                        SAL_INFO("ucb.ucp.webdav.curl",
                                 "PROPFIND: exception parsing uri " << it.uri);
                    }
                }
            }
        }
        else
        {
            *::std::get<2>(*o_pRequestedProperties) = parseWebDAVLockResponse(xResponseInStream);
        }
    }
}
 
// DAV methods
auto CurlSession::PROPFIND(OUString const& rURIReference, Depth const depth,
                           ::std::vector<OUString> const& rPropertyNames,
                           ::std::vector<DAVResource>& o_rResources,
                           DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "PROPFIND: " << rURIReference << " " << depth);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    ::std::tuple<::std::vector<OUString> const&, ::std::vector<DAVResource>* const,
                 ::std::vector<ucb::Lock>* const> const args(rPropertyNames, &o_rResources,
                                                             nullptr);
    return CurlProcessor::PropFind(*this, uri, depth, &args, nullptr, rEnv);
}
 
auto CurlSession::PROPFIND(OUString const& rURIReference, Depth const depth,
                           ::std::vector<DAVResourceInfo>& o_rResourceInfos,
                           DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "PROPFIND: " << rURIReference << " " << depth);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    return CurlProcessor::PropFind(*this, uri, depth, nullptr, &o_rResourceInfos, rEnv);
}
 
auto CurlSession::PROPPATCH(OUString const& rURIReference,
                            ::std::vector<ProppatchValue> const& rValues,
                            DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "PROPPATCH: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    // TODO: either set CURLOPT_INFILESIZE_LARGE or chunked?
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList;
    pList.reset(curl_slist_append(pList.release(), "Content-Type: application/xml"));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
 
    // generate XML document for PROPPATCH
    uno::Reference<io::XSequenceOutputStream> const xSeqOutStream(
        io::SequenceOutputStream::create(m_xContext));
    uno::Reference<io::XOutputStream> const xRequestOutStream(xSeqOutStream);
    assert(xRequestOutStream.is());
    uno::Reference<xml::sax::XWriter> const xWriter(xml::sax::Writer::create(m_xContext));
    xWriter->setOutputStream(xRequestOutStream);
    xWriter->startDocument();
    rtl::Reference<::comphelper::AttributeList> const pAttrList(new ::comphelper::AttributeList);
    pAttrList->AddAttribute(u"xmlns"_ustr, u"DAV:"_ustr);
    xWriter->startElement(u"propertyupdate"_ustr, pAttrList);
    for (ProppatchValue const& rPropValue : rValues)
    {
        assert(rPropValue.operation == PROPSET || rPropValue.operation == PROPREMOVE);
        OUString const operation((rPropValue.operation == PROPSET) ? u"set"_ustr : u"remove"_ustr);
        xWriter->startElement(operation, nullptr);
        xWriter->startElement(u"prop"_ustr, nullptr);
        SerfPropName name;
        DAVProperties::createSerfPropName(rPropValue.name, name);
        pAttrList->Clear();
        pAttrList->AddAttribute(u"xmlns"_ustr, OUString::createFromAscii(name.nspace));
        xWriter->startElement(OUString::createFromAscii(name.name), pAttrList);
        if (rPropValue.operation == PROPSET)
        {
            if (DAVProperties::isUCBDeadProperty(name))
            {
                ::std::optional<::std::pair<OUString, OUString>> const oProp(
                    UCBDeadPropertyValue::toXML(rPropValue.value));
                if (oProp)
                {
                    xWriter->startElement(u"ucbprop"_ustr, nullptr);
                    xWriter->startElement(u"type"_ustr, nullptr);
                    xWriter->characters(oProp->first);
                    xWriter->endElement(u"type"_ustr);
                    xWriter->startElement(u"value"_ustr, nullptr);
                    xWriter->characters(oProp->second);
                    xWriter->endElement(u"value"_ustr);
                    xWriter->endElement(u"ucbprop"_ustr);
                }
            }
            else
            {
                OUString value;
                rPropValue.value >>= value;
                xWriter->characters(value);
            }
        }
        xWriter->endElement(OUString::createFromAscii(name.name));
        xWriter->endElement(u"prop"_ustr);
        xWriter->endElement(operation);
    }
    xWriter->endElement(u"propertyupdate"_ustr);
    xWriter->endDocument();
 
    uno::Reference<io::XInputStream> const xRequestInStream(
        io::SequenceInputStream::createStreamFromSequence(m_xContext,
                                                          xSeqOutStream->getWrittenBytes()));
    assert(xRequestInStream.is());
 
    curl_off_t const len(xSeqOutStream->getWrittenBytes().getLength());
 
    ::std::vector<CurlOption> const options{
        { CURLOPT_UPLOAD, 1L, nullptr },
        { CURLOPT_CUSTOMREQUEST, "PROPPATCH", "CURLOPT_CUSTOMREQUEST" },
        // note: Sharepoint cannot handle "Transfer-Encoding: chunked"
        { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT }
    };
 
    CurlProcessor::ProcessRequest(*this, uri, u"PROPPATCH"_ustr, options, &rEnv, ::std::move(pList),
                                  nullptr, &xRequestInStream, nullptr);
}
 
auto CurlSession::HEAD(OUString const& rURIReference, ::std::vector<OUString> const& rHeaderNames,
                       DAVResource& io_rResource, DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "HEAD: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    ::std::vector<CurlOption> const options{
        // NOBODY will prevent logging the response body in ProcessRequest()
        // exception, but omitting it here results in a long timeout until the
        // server closes the connection, which is worse
        { CURLOPT_NOBODY, 1L, nullptr },
        { CURLOPT_CUSTOMREQUEST, "HEAD", "CURLOPT_CUSTOMREQUEST" }
    };
 
    ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(rHeaderNames,
                                                                            io_rResource);
 
    CurlProcessor::ProcessRequest(*this, uri, u"HEAD"_ustr, options, &rEnv, nullptr, nullptr,
                                  nullptr, &headers);
}
 
auto CurlSession::GET(OUString const& rURIReference, DAVRequestEnvironment const& rEnv)
    -> uno::Reference<io::XInputStream>
{
    SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    // could use either com.sun.star.io.Pipe or com.sun.star.io.SequenceInputStream?
    // Pipe can just write into its XOuputStream, which is simpler.
    // Both resize exponentially, so performance should be fine.
    // However, Pipe doesn't implement XSeekable, which is required by filters.
 
    uno::Reference<io::XSequenceOutputStream> const xSeqOutStream(
        io::SequenceOutputStream::create(m_xContext));
    uno::Reference<io::XOutputStream> const xResponseOutStream(xSeqOutStream);
    assert(xResponseOutStream.is());
 
    ::std::vector<CurlOption> const options{ { CURLOPT_HTTPGET, 1L, nullptr } };
 
    CurlProcessor::ProcessRequest(*this, uri, u"GET"_ustr, options, &rEnv, nullptr,
                                  &xResponseOutStream, nullptr, nullptr);
 
    uno::Reference<io::XInputStream> const xResponseInStream(
        io::SequenceInputStream::createStreamFromSequence(m_xContext,
                                                          xSeqOutStream->getWrittenBytes()));
    assert(xResponseInStream.is());
 
    return xResponseInStream;
}
 
auto CurlSession::GET(OUString const& rURIReference, uno::Reference<io::XOutputStream>& rxOutStream,
                      DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    ::std::vector<CurlOption> const options{ { CURLOPT_HTTPGET, 1L, nullptr } };
 
    CurlProcessor::ProcessRequest(*this, uri, u"GET"_ustr, options, &rEnv, nullptr, &rxOutStream,
                                  nullptr, nullptr);
}
 
auto CurlSession::GET(OUString const& rURIReference, ::std::vector<OUString> const& rHeaderNames,
                      DAVResource& io_rResource, DAVRequestEnvironment const& rEnv)
    -> uno::Reference<io::XInputStream>
{
    SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    ::std::vector<CurlOption> const options{ { CURLOPT_HTTPGET, 1L, nullptr } };
 
    uno::Reference<io::XSequenceOutputStream> const xSeqOutStream(
        io::SequenceOutputStream::create(m_xContext));
    uno::Reference<io::XOutputStream> const xResponseOutStream(xSeqOutStream);
    assert(xResponseOutStream.is());
 
    ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(rHeaderNames,
                                                                            io_rResource);
 
    CurlProcessor::ProcessRequest(*this, uri, u"GET"_ustr, options, &rEnv, nullptr,
                                  &xResponseOutStream, nullptr, &headers);
 
    uno::Reference<io::XInputStream> const xResponseInStream(
        io::SequenceInputStream::createStreamFromSequence(m_xContext,
                                                          xSeqOutStream->getWrittenBytes()));
    assert(xResponseInStream.is());
 
    return xResponseInStream;
}
 
auto CurlSession::GET(OUString const& rURIReference, uno::Reference<io::XOutputStream>& rxOutStream,
                      ::std::vector<OUString> const& rHeaderNames, DAVResource& io_rResource,
                      DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "GET: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    ::std::vector<CurlOption> const options{ { CURLOPT_HTTPGET, 1L, nullptr } };
 
    ::std::pair<::std::vector<OUString> const&, DAVResource&> const headers(rHeaderNames,
                                                                            io_rResource);
 
    CurlProcessor::ProcessRequest(*this, uri, u"GET"_ustr, options, &rEnv, nullptr, &rxOutStream,
                                  nullptr, &headers);
}
 
auto CurlSession::PUT(OUString const& rURIReference,
                      uno::Reference<io::XInputStream> const& rxInStream,
                      DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "PUT: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    // NextCloud silently fails with chunked encoding
    uno::Reference<io::XSeekable> const xSeekable(rxInStream, uno::UNO_QUERY);
    if (!xSeekable.is())
    {
        throw uno::RuntimeException(u"TODO: not seekable"_ustr);
    }
    curl_off_t const len(xSeekable->getLength() - xSeekable->getPosition());
 
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList;
    OUString const* const pToken(g_Init.LockStore.getLockTokenForURI(uri.GetURI(), nullptr));
    if (pToken)
    {
        OString const utf8If("If: "
        // disabled as Sharepoint 2013 workaround, it accepts only
        // "No-Tag-List", see fed2984281a85a5a2f308841ec810f218c75f2ab
#if 0
                "<" + OUStringToOString(rURIReference, RTL_TEXTENCODING_ASCII_US)
                             + "> "
#endif
                             "(<"
                             + OUStringToOString(*pToken, RTL_TEXTENCODING_ASCII_US) + ">)");
        pList.reset(curl_slist_append(pList.release(), utf8If.getStr()));
        if (!pList)
        {
            throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
        }
    }
 
    // lock m_Mutex after accessing global LockStore to avoid deadlock
 
    // note: Nextcloud 20 cannot handle "Transfer-Encoding: chunked"
    ::std::vector<CurlOption> const options{
        { CURLOPT_UPLOAD, 1L, nullptr }, // libcurl won't upload without setting this
        { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT }
    };
 
    CurlProcessor::ProcessRequest(*this, uri, u"PUT"_ustr, options, &rEnv, ::std::move(pList),
                                  nullptr, &rxInStream, nullptr);
}
 
auto CurlSession::POST(OUString const& rURIReference, OUString const& rContentType,
                       OUString const& rReferer, uno::Reference<io::XInputStream> const& rxInStream,
                       DAVRequestEnvironment const& rEnv) -> uno::Reference<io::XInputStream>
{
    SAL_INFO("ucb.ucp.webdav.curl", "POST: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    // TODO: either set CURLOPT_POSTFIELDSIZE_LARGE or chunked?
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList(
        curl_slist_append(nullptr, "Transfer-Encoding: chunked"));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
    OString const utf8ContentType("Content-Type: "
                                  + OUStringToOString(rContentType, RTL_TEXTENCODING_ASCII_US));
    pList.reset(curl_slist_append(pList.release(), utf8ContentType.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
    OString const utf8Referer("Referer: " + OUStringToOString(rReferer, RTL_TEXTENCODING_ASCII_US));
    pList.reset(curl_slist_append(pList.release(), utf8Referer.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
 
    ::std::vector<CurlOption> const options{ { CURLOPT_POST, 1L, nullptr } };
 
    uno::Reference<io::XSequenceOutputStream> const xSeqOutStream(
        io::SequenceOutputStream::create(m_xContext));
    uno::Reference<io::XOutputStream> const xResponseOutStream(xSeqOutStream);
    assert(xResponseOutStream.is());
 
    CurlProcessor::ProcessRequest(*this, uri, u"POST"_ustr, options, &rEnv, ::std::move(pList),
                                  &xResponseOutStream, &rxInStream, nullptr);
 
    uno::Reference<io::XInputStream> const xResponseInStream(
        io::SequenceInputStream::createStreamFromSequence(m_xContext,
                                                          xSeqOutStream->getWrittenBytes()));
    assert(xResponseInStream.is());
 
    return xResponseInStream;
}
 
auto CurlSession::POST(OUString const& rURIReference, OUString const& rContentType,
                       OUString const& rReferer, uno::Reference<io::XInputStream> const& rxInStream,
                       uno::Reference<io::XOutputStream>& rxOutStream,
                       DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "POST: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    // TODO: either set CURLOPT_POSTFIELDSIZE_LARGE or chunked?
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList(
        curl_slist_append(nullptr, "Transfer-Encoding: chunked"));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
    OString const utf8ContentType("Content-Type: "
                                  + OUStringToOString(rContentType, RTL_TEXTENCODING_ASCII_US));
    pList.reset(curl_slist_append(pList.release(), utf8ContentType.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
    OString const utf8Referer("Referer: " + OUStringToOString(rReferer, RTL_TEXTENCODING_ASCII_US));
    pList.reset(curl_slist_append(pList.release(), utf8Referer.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
 
    ::std::vector<CurlOption> const options{ { CURLOPT_POST, 1L, nullptr } };
 
    CurlProcessor::ProcessRequest(*this, uri, u"POST"_ustr, options, &rEnv, ::std::move(pList),
                                  &rxOutStream, &rxInStream, nullptr);
}
 
auto CurlSession::MKCOL(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "MKCOL: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    ::std::vector<CurlOption> const options{ { CURLOPT_CUSTOMREQUEST, "MKCOL",
                                               "CURLOPT_CUSTOMREQUEST" } };
 
    CurlProcessor::ProcessRequest(*this, uri, u"MKCOL"_ustr, options, &rEnv, nullptr, nullptr,
                                  nullptr, nullptr);
}
 
auto CurlProcessor::MoveOrCopy(CurlSession& rSession, std::u16string_view rSourceURIReference,
                               ::std::u16string_view const rDestinationURI,
                               DAVRequestEnvironment const& rEnv, bool const isOverwrite,
                               char const* const pMethod) -> void
{
    CurlUri const uriSource(CurlProcessor::URIReferenceToURI(rSession, rSourceURIReference));
 
    OString const utf8Destination("Destination: "
                                  + OUStringToOString(rDestinationURI, RTL_TEXTENCODING_ASCII_US));
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList(
        curl_slist_append(nullptr, utf8Destination.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
    OString const utf8Overwrite(OString::Concat("Overwrite: ") + (isOverwrite ? "T" : "F"));
    pList.reset(curl_slist_append(pList.release(), utf8Overwrite.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
 
    ::std::vector<CurlOption> const options{ { CURLOPT_CUSTOMREQUEST, pMethod,
                                               "CURLOPT_CUSTOMREQUEST" } };
 
    CurlProcessor::ProcessRequest(rSession, uriSource, OUString::createFromAscii(pMethod), options,
                                  &rEnv, ::std::move(pList), nullptr, nullptr, nullptr);
}
 
auto CurlSession::COPY(OUString const& rSourceURIReference, OUString const& rDestinationURI,
                       DAVRequestEnvironment const& rEnv, bool const isOverwrite) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "COPY: " << rSourceURIReference);
 
    return CurlProcessor::MoveOrCopy(*this, rSourceURIReference, rDestinationURI, rEnv, isOverwrite,
                                     "COPY");
}
 
auto CurlSession::MOVE(OUString const& rSourceURIReference, OUString const& rDestinationURI,
                       DAVRequestEnvironment const& rEnv, bool const isOverwrite) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "MOVE: " << rSourceURIReference);
 
    return CurlProcessor::MoveOrCopy(*this, rSourceURIReference, rDestinationURI, rEnv, isOverwrite,
                                     "MOVE");
}
 
auto CurlSession::DESTROY(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "DESTROY: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    ::std::vector<CurlOption> const options{ { CURLOPT_CUSTOMREQUEST, "DELETE",
                                               "CURLOPT_CUSTOMREQUEST" } };
 
    CurlProcessor::ProcessRequest(*this, uri, u"DESTROY"_ustr, options, &rEnv, nullptr, nullptr,
                                  nullptr, nullptr);
}
 
auto CurlProcessor::Lock(
    CurlSession& rSession, CurlUri const& rURI, DAVRequestEnvironment const* const pEnv,
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>>
        pRequestHeaderList,
    uno::Reference<io::XInputStream> const* const pxRequestInStream)
    -> ::std::vector<::std::pair<ucb::Lock, sal_Int32>>
{
    curl_off_t len(0);
    if (pxRequestInStream)
    {
        uno::Reference<io::XSeekable> const xSeekable(*pxRequestInStream, uno::UNO_QUERY);
        assert(xSeekable.is());
        len = xSeekable->getLength();
    }
 
    ::std::vector<CurlOption> const options{
        { CURLOPT_UPLOAD, 1L, nullptr },
        { CURLOPT_CUSTOMREQUEST, "LOCK", "CURLOPT_CUSTOMREQUEST" },
        // note: Sharepoint cannot handle "Transfer-Encoding: chunked"
        { CURLOPT_INFILESIZE_LARGE, len, nullptr, CurlOption::Type::CurlOffT }
    };
 
    // stream for response
    uno::Reference<io::XInputStream> const xResponseInStream(io::Pipe::create(rSession.m_xContext));
    uno::Reference<io::XOutputStream> const xResponseOutStream(xResponseInStream, uno::UNO_QUERY);
    assert(xResponseInStream.is());
    assert(xResponseOutStream.is());
 
    TimeValue startTime;
    osl_getSystemTime(&startTime);
 
    CurlProcessor::ProcessRequest(rSession, rURI, u"LOCK"_ustr, options, pEnv,
                                  ::std::move(pRequestHeaderList), &xResponseOutStream,
                                  pxRequestInStream, nullptr);
 
    ::std::vector<ucb::Lock> const acquiredLocks(parseWebDAVLockResponse(xResponseInStream));
    SAL_WARN_IF(acquiredLocks.empty(), "ucb.ucp.webdav.curl",
                "could not get LOCK for " << rURI.GetURI());
 
    TimeValue endTime;
    osl_getSystemTime(&endTime);
    auto const elapsedSeconds(endTime.Seconds - startTime.Seconds);
 
    // determine expiration time (seconds from endTime) for each acquired lock
    ::std::vector<::std::pair<ucb::Lock, sal_Int32>> ret;
    ret.reserve(acquiredLocks.size());
    for (auto const& rLock : acquiredLocks)
    {
        sal_Int32 lockExpirationTimeSeconds;
        if (rLock.Timeout == -1)
        {
            lockExpirationTimeSeconds = -1;
        }
        else if (rLock.Timeout <= elapsedSeconds)
        {
            SAL_WARN("ucb.ucp.webdav.curl",
                     "LOCK timeout already expired when receiving LOCK response for "
                         << rURI.GetURI());
            lockExpirationTimeSeconds = 0;
        }
        else
        {
            lockExpirationTimeSeconds = startTime.Seconds + rLock.Timeout;
        }
        ret.emplace_back(rLock, lockExpirationTimeSeconds);
    }
 
    return ret;
}
 
auto CurlSession::LOCK(OUString const& rURIReference, ucb::Lock /*const*/& rLock,
                       DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "LOCK: " << rURIReference);
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    if (g_Init.LockStore.getLockTokenForURI(uri.GetURI(), &rLock))
    {
        // already have a lock that covers the requirement
        // TODO: maybe use DAV:lockdiscovery to ensure it's valid
        return;
    }
 
    // note: no m_Mutex lock needed here, only in CurlProcessor::Lock()
 
    // generate XML document for acquiring new LOCK
    uno::Reference<io::XSequenceOutputStream> const xSeqOutStream(
        io::SequenceOutputStream::create(m_xContext));
    uno::Reference<io::XOutputStream> const xRequestOutStream(xSeqOutStream);
    assert(xRequestOutStream.is());
    uno::Reference<xml::sax::XWriter> const xWriter(xml::sax::Writer::create(m_xContext));
    xWriter->setOutputStream(xRequestOutStream);
    xWriter->startDocument();
    rtl::Reference<::comphelper::AttributeList> const pAttrList(new ::comphelper::AttributeList);
    pAttrList->AddAttribute(u"xmlns"_ustr, u"DAV:"_ustr);
    xWriter->startElement(u"lockinfo"_ustr, pAttrList);
    xWriter->startElement(u"lockscope"_ustr, nullptr);
    switch (rLock.Scope)
    {
        case ucb::LockScope_EXCLUSIVE:
            xWriter->startElement(u"exclusive"_ustr, nullptr);
            xWriter->endElement(u"exclusive"_ustr);
            break;
        case ucb::LockScope_SHARED:
            xWriter->startElement(u"shared"_ustr, nullptr);
            xWriter->endElement(u"shared"_ustr);
            break;
        default:
            assert(false);
    }
    xWriter->endElement(u"lockscope"_ustr);
    xWriter->startElement(u"locktype"_ustr, nullptr);
    xWriter->startElement(u"write"_ustr, nullptr);
    xWriter->endElement(u"write"_ustr);
    xWriter->endElement(u"locktype"_ustr);
    OUString owner;
    if ((rLock.Owner >>= owner) && !owner.isEmpty())
    {
        xWriter->startElement(u"owner"_ustr, nullptr);
        xWriter->characters(owner);
        xWriter->endElement(u"owner"_ustr);
    }
    xWriter->endElement(u"lockinfo"_ustr);
    xWriter->endDocument();
 
    uno::Reference<io::XInputStream> const xRequestInStream(
        io::SequenceInputStream::createStreamFromSequence(m_xContext,
                                                          xSeqOutStream->getWrittenBytes()));
    assert(xRequestInStream.is());
 
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList;
    pList.reset(curl_slist_append(pList.release(), "Content-Type: application/xml"));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
    OString depth;
    switch (rLock.Depth)
    {
        case ucb::LockDepth_ZERO:
            depth = "Depth: 0"_ostr;
            break;
        case ucb::LockDepth_ONE:
            depth = "Depth: 1"_ostr;
            break;
        case ucb::LockDepth_INFINITY:
            depth = "Depth: infinity"_ostr;
            break;
        default:
            assert(false);
    }
    pList.reset(curl_slist_append(pList.release(), depth.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
    OString timeout;
    switch (rLock.Timeout)
    {
        case -1:
            timeout = "Timeout: Infinite"_ostr;
            break;
        case 0:
            timeout = "Timeout: Second-180"_ostr;
            break;
        default:
            timeout = "Timeout: Second-" + OString::number(rLock.Timeout);
            assert(0 < rLock.Timeout);
            break;
    }
    pList.reset(curl_slist_append(pList.release(), timeout.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
 
    auto const acquiredLocks
        = CurlProcessor::Lock(*this, uri, &rEnv, ::std::move(pList), &xRequestInStream);
 
    for (auto const& rAcquiredLock : acquiredLocks)
    {
        g_Init.LockStore.addLock(uri.GetURI(), rAcquiredLock.first,
                                 rAcquiredLock.first.LockTokens[0], this, rAcquiredLock.second);
        SAL_INFO("ucb.ucp.webdav.curl", "created LOCK for " << rURIReference);
    }
}
 
auto CurlProcessor::Unlock(CurlSession& rSession, CurlUri const& rURI,
                           DAVRequestEnvironment const* const pEnv) -> void
{
    OUString const* const pToken(g_Init.LockStore.getLockTokenForURI(rURI.GetURI(), nullptr));
    if (!pToken)
    {
        SAL_WARN("ucb.ucp.webdav.curl", "attempt to unlock but not locked");
        throw DAVException(DAVException::DAV_NOT_LOCKED);
    }
    OString const utf8LockToken("Lock-Token: <"
                                + OUStringToOString(*pToken, RTL_TEXTENCODING_ASCII_US) + ">");
    ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList(
        curl_slist_append(nullptr, utf8LockToken.getStr()));
    if (!pList)
    {
        throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
    }
 
    ::std::vector<CurlOption> const options{ { CURLOPT_CUSTOMREQUEST, "UNLOCK",
                                               "CURLOPT_CUSTOMREQUEST" } };
 
    CurlProcessor::ProcessRequest(rSession, rURI, u"UNLOCK"_ustr, options, pEnv, ::std::move(pList),
                                  nullptr, nullptr, nullptr);
}
 
auto CurlSession::UNLOCK(OUString const& rURIReference, DAVRequestEnvironment const& rEnv) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "UNLOCK: " << rURIReference);
 
    // note: no m_Mutex lock needed here, only in CurlProcessor::Unlock()
 
    CurlUri const uri(CurlProcessor::URIReferenceToURI(*this, rURIReference));
 
    CurlProcessor::Unlock(*this, uri, &rEnv);
 
    g_Init.LockStore.removeLock(uri.GetURI());
}
 
auto CurlSession::NonInteractive_LOCK(OUString const& rURI, ::std::u16string_view const rLockToken,
                                      sal_Int32& o_rLastChanceToSendRefreshRequest,
                                      bool& o_rIsAuthFailed) -> bool
{
    SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK: " << rURI);
 
    // note: no m_Mutex lock needed here, only in CurlProcessor::Lock()
 
    try
    {
        CurlUri const uri(rURI);
        ::std::unique_ptr<curl_slist, deleter_from_fn<curl_slist, curl_slist_free_all>> pList(
            curl_slist_append(nullptr, "Timeout: Second-180"));
 
        assert(!rLockToken.empty()); // LockStore is the caller
        OString const utf8If("If: (<" + OUStringToOString(rLockToken, RTL_TEXTENCODING_ASCII_US)
                             + ">)");
        pList.reset(curl_slist_append(pList.release(), utf8If.getStr()));
        if (!pList)
        {
            throw uno::RuntimeException(u"curl_slist_append failed"_ustr);
        }
 
        auto const acquiredLocks
            = CurlProcessor::Lock(*this, uri, nullptr, ::std::move(pList), nullptr);
 
        SAL_WARN_IF(1 < acquiredLocks.size(), "ucb.ucp.webdav.curl",
                    "multiple locks acquired on refresh for " << rURI);
        if (!acquiredLocks.empty())
        {
            o_rLastChanceToSendRefreshRequest = acquiredLocks.begin()->second;
        }
        SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK succeeded on " << rURI);
        return true;
    }
    catch (DAVException const& rException)
    {
        SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK failed on " << rURI);
        switch (rException.getError())
        {
            case DAVException::DAV_HTTP_AUTH:
            case DAVException::DAV_HTTP_NOAUTH:
                o_rIsAuthFailed = true;
                break;
            default:
                break;
        }
        return false;
    }
    catch (...)
    {
        SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_LOCK failed on " << rURI);
        return false;
    }
}
 
auto CurlSession::NonInteractive_UNLOCK(OUString const& rURI) -> void
{
    SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK: " << rURI);
 
    // note: no m_Mutex lock needed here, only in CurlProcessor::Unlock()
 
    try
    {
        CurlUri const uri(rURI);
 
        CurlProcessor::Unlock(*this, uri, nullptr);
 
        // the only caller is the dtor of the LockStore, don't call remove!
        SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK succeeded on " << rURI);
    }
    catch (...)
    {
        SAL_INFO("ucb.ucp.webdav.curl", "NonInteractive_UNLOCK failed on " << rURI);
    }
}
 
} // namespace http_dav_ucp
 
namespace
{
/// Manage lifecycle of global DAV worker threads
class WebDAVManager : public cppu::WeakImplHelper<css::lang::XServiceInfo>,
                      public comphelper::LibreOfficeKit::ThreadJoinable
{
public:
    WebDAVManager() {}
 
    // XServiceInfo
    virtual OUString SAL_CALL getImplementationName() override
    {
        return "com.sun.star.comp.WebDAVManager";
    }
    virtual sal_Bool SAL_CALL supportsService(const OUString& ServiceName) override
    {
        return cppu::supportsService(this, ServiceName);
    }
    virtual css::uno::Sequence<OUString> SAL_CALL getSupportedServiceNames() override
    {
        return { "com.sun.star.ucb.WebDAVManager" };
    }
 
    // comphelper::LibreOfficeKit::ThreadJoinable
    virtual bool joinThreads() override { return g_Init.LockStore.joinThreads(); }
 
    virtual void startThreads() override { g_Init.LockStore.startThreads(); }
};
 
} // anonymous namespace
 
extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface*
ucb_webdav_manager_get_implementation(css::uno::XComponentContext*,
                                      css::uno::Sequence<css::uno::Any> const&)
{
    return cppu::acquire(new WebDAVManager());
}
 
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

V530 The return value of function 'append' is required to be utilized.

V654 The condition 'nRunning != 0' of loop is always true.

V547 Expression '!"curl_global_init failed"' is always false.

V547 Expression '!"curl_share_init failed"' is always false.

V547 Expression '!"curl_share_setopt failed"' is always false.

V547 Expression '!"curl_share_setopt failed"' is always false.

V547 Expression '!"curl_share_setopt failed"' is always false.