cool#9992 lok doc sign, hash extract: initial getCommandValues('Signature')
The trouble with signing via ca/cert/key PEM files is that usually the CA is not trusted by the received of the signature. 3rd-party services are available to do generate trusted signatures, but then you need to share your document with them, which can be also problematic. A middle-ground here is to sign the hash of the document by a 3rd-party, something that's supported by e.g. <https://docs.eideasy.com/electronic-signatures/api-flow-with-file-hashes-pdf.html> (which itself aggregates a number of providers). As a first step, add LOK API to get what would be the signature time during signing -- but instead of actually signing, just return this information. Once the same is done with the doc hash, this is supposed to provide the same info than what the reference <https://github.com/eideasy/eideasy-external-pades-digital-signatures> app does. This is only a start: incrementally replace XCertificate with SignatureContext, which allows aborting the signing right before calling into NSS, and also later it'll allow injecting the PKCS#7 object we get from the 3rd-party. Change-Id: I108564f047fdb4fb796240c7d18a584cd9044313 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/176279 Tested-by: Jenkins Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
This commit is contained in:
parent
e44f566a2c
commit
12e5082537
15 changed files with 107 additions and 29 deletions
|
@ -74,6 +74,7 @@
|
|||
#include <rtl/bootstrap.hxx>
|
||||
#include <rtl/strbuf.hxx>
|
||||
#include <rtl/uri.hxx>
|
||||
#include <svl/cryptosign.hxx>
|
||||
#include <linguistic/misc.hxx>
|
||||
#include <cppuhelper/bootstrap.hxx>
|
||||
#include <comphelper/random.hxx>
|
||||
|
@ -6836,6 +6837,12 @@ static char* doc_getCommandValues(LibreOfficeKitDocument* pThis, const char* pCo
|
|||
pDoc->getCommandValues(aJsonWriter, aCommand);
|
||||
return convertOString(aJsonWriter.finishAndGetAsOString());
|
||||
}
|
||||
else if (SfxLokHelper::supportsCommand(INetURLObject(OUString::fromUtf8(aCommand)).GetURLPath()))
|
||||
{
|
||||
tools::JsonWriter aJsonWriter;
|
||||
SfxLokHelper::getCommandValues(aJsonWriter, aCommand);
|
||||
return convertOString(aJsonWriter.finishAndGetAsOString());
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLastExceptionMsg(OUString::fromUtf8(aCommand) + u" : Unknown command, no values returned"_ustr);
|
||||
|
@ -7272,7 +7279,9 @@ static bool doc_insertCertificate(LibreOfficeKitDocument* pThis,
|
|||
|
||||
SolarMutexGuard aGuard;
|
||||
|
||||
return pObjectShell->SignDocumentContentUsingCertificate(xCertificate);
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
aSigningContext.m_xCertificate = xCertificate;
|
||||
return pObjectShell->SignDocumentContentUsingCertificate(aSigningContext);
|
||||
}
|
||||
|
||||
static bool doc_addCertificate(LibreOfficeKitDocument* pThis,
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
#include <sal/types.h>
|
||||
|
||||
class SfxViewShell;
|
||||
namespace svl::crypto
|
||||
{
|
||||
class SigningContext;
|
||||
}
|
||||
|
||||
namespace sfx2
|
||||
{
|
||||
|
@ -27,11 +31,10 @@ class SAL_NO_VTABLE SAL_DLLPUBLIC_RTTI SAL_LOPLUGIN_ANNOTATE("crosscast") Digita
|
|||
{
|
||||
public:
|
||||
/// Same as signDocumentWithCertificate(), but passes the xModel as well.
|
||||
virtual bool
|
||||
SignModelWithCertificate(const css::uno::Reference<css::frame::XModel>& xModel,
|
||||
const css::uno::Reference<css::security::XCertificate>& xCertificate,
|
||||
const css::uno::Reference<css::embed::XStorage>& xStorage,
|
||||
const css::uno::Reference<css::io::XStream>& xStream)
|
||||
virtual bool SignModelWithCertificate(const css::uno::Reference<css::frame::XModel>& xModel,
|
||||
svl::crypto::SigningContext& rSigningContext,
|
||||
const css::uno::Reference<css::embed::XStorage>& xStorage,
|
||||
const css::uno::Reference<css::io::XStream>& xStream)
|
||||
= 0;
|
||||
|
||||
/// Async replacement for signDocumentContent().
|
||||
|
|
|
@ -47,6 +47,7 @@ namespace com::sun::star::frame
|
|||
class XModel;
|
||||
}
|
||||
namespace ucbhelper { class Content; }
|
||||
namespace svl::crypto { class SigningContext; }
|
||||
|
||||
class SvKeyValueIterator;
|
||||
class SfxFilter;
|
||||
|
@ -289,7 +290,7 @@ public:
|
|||
|
||||
SAL_DLLPRIVATE bool SignDocumentContentUsingCertificate(
|
||||
const css::uno::Reference<css::frame::XModel>& xModel, bool bHasValidDocumentSignature,
|
||||
const css::uno::Reference<css::security::XCertificate>& xCertificate);
|
||||
svl::crypto::SigningContext& rSigningContext);
|
||||
|
||||
// the following two methods must be used and make sense only during saving currently
|
||||
// TODO/LATER: in future the signature state should be controlled by the medium not by the document
|
||||
|
|
|
@ -249,6 +249,10 @@ public:
|
|||
static void addCertificates(const std::vector<std::string>& rCerts);
|
||||
/// Parses a private key + certificate pair.
|
||||
static css::uno::Reference<css::security::XCertificate> getSigningCertificate(const std::string& rCert, const std::string& rKey);
|
||||
/// Decides if it's OK to call getCommandValues(rCommand).
|
||||
static bool supportsCommand(std::u16string_view rCommand);
|
||||
/// Returns information about a given command in JSON format.
|
||||
static void getCommandValues(tools::JsonWriter& rJsonWriter, std::string_view rCommand);
|
||||
|
||||
private:
|
||||
static int createView(SfxViewFrame& rViewFrame, ViewShellDocId docId);
|
||||
|
|
|
@ -147,6 +147,7 @@ namespace o3tl
|
|||
}
|
||||
|
||||
namespace weld { class Window; }
|
||||
namespace svl::crypto { class SigningContext; }
|
||||
|
||||
enum class HiddenWarningFact
|
||||
{
|
||||
|
@ -368,7 +369,7 @@ public:
|
|||
const css::uno::Reference<css::security::XDocumentDigitalSignatures>& xSigner
|
||||
= css::uno::Reference<css::security::XDocumentDigitalSignatures>());
|
||||
|
||||
bool SignDocumentContentUsingCertificate(const css::uno::Reference<css::security::XCertificate>& xCertificate);
|
||||
bool SignDocumentContentUsingCertificate(svl::crypto::SigningContext& rSigningContext);
|
||||
bool ResignDocument(css::uno::Sequence< css::security::DocumentSignatureInformation >& rSignaturesInfo);
|
||||
|
||||
void SignSignatureLine(weld::Window* pDialogParent, const OUString& aSignatureLineId,
|
||||
|
|
|
@ -92,6 +92,17 @@ private:
|
|||
OUString m_aSignPassword;
|
||||
};
|
||||
|
||||
/// Wrapper around a certificate: allows either an actual signing or extracting enough info, so a
|
||||
/// 3rd-party can sign our document.
|
||||
class SVL_DLLPUBLIC SigningContext
|
||||
{
|
||||
public:
|
||||
/// If set, the certificate used for signing.
|
||||
css::uno::Reference<css::security::XCertificate> m_xCertificate;
|
||||
/// If m_xCertificate is not set, the time that would be used.
|
||||
sal_Int64 m_nSignatureTime = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|
||||
|
|
|
@ -132,6 +132,7 @@
|
|||
#include <sfx2/viewfrm.hxx>
|
||||
#include <comphelper/threadpool.hxx>
|
||||
#include <o3tl/string_view.hxx>
|
||||
#include <svl/cryptosign.hxx>
|
||||
#include <condition_variable>
|
||||
|
||||
#include <com/sun/star/io/WrongFormatException.hpp>
|
||||
|
@ -4184,7 +4185,7 @@ void SfxMedium::CreateTempFileNoCopy()
|
|||
|
||||
bool SfxMedium::SignDocumentContentUsingCertificate(
|
||||
const css::uno::Reference<css::frame::XModel>& xModel, bool bHasValidDocumentSignature,
|
||||
const Reference<XCertificate>& xCertificate)
|
||||
svl::crypto::SigningContext& rSigningContext)
|
||||
{
|
||||
bool bChanges = false;
|
||||
|
||||
|
@ -4252,7 +4253,7 @@ bool SfxMedium::SignDocumentContentUsingCertificate(
|
|||
xStream.set(xMetaInf->openStreamElement(xSigner->getDocumentContentSignatureDefaultStreamName(), embed::ElementModes::READWRITE), uno::UNO_SET_THROW);
|
||||
|
||||
bool bSuccess = xModelSigner->SignModelWithCertificate(
|
||||
xModel, xCertificate, GetZipStorageToSign_Impl(), xStream);
|
||||
xModel, rSigningContext, GetZipStorageToSign_Impl(), xStream);
|
||||
|
||||
if (bSuccess)
|
||||
{
|
||||
|
@ -4273,7 +4274,7 @@ bool SfxMedium::SignDocumentContentUsingCertificate(
|
|||
|
||||
// We need read-write to be able to add the signature relation.
|
||||
bool bSuccess = xModelSigner->SignModelWithCertificate(
|
||||
xModel, xCertificate, GetZipStorageToSign_Impl(/*bReadOnly=*/false), xStream);
|
||||
xModel, rSigningContext, GetZipStorageToSign_Impl(/*bReadOnly=*/false), xStream);
|
||||
|
||||
if (bSuccess)
|
||||
{
|
||||
|
@ -4291,7 +4292,7 @@ bool SfxMedium::SignDocumentContentUsingCertificate(
|
|||
std::unique_ptr<SvStream> pStream(utl::UcbStreamHelper::CreateStream(GetName(), StreamMode::READ | StreamMode::WRITE));
|
||||
uno::Reference<io::XStream> xStream(new utl::OStreamWrapper(*pStream));
|
||||
if (xModelSigner->SignModelWithCertificate(
|
||||
xModel, xCertificate, uno::Reference<embed::XStorage>(), xStream))
|
||||
xModel, rSigningContext, uno::Reference<embed::XStorage>(), xStream))
|
||||
bChanges = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,6 +101,7 @@
|
|||
#include <com/sun/star/system/SystemShellExecuteFlags.hpp>
|
||||
|
||||
#include <osl/file.hxx>
|
||||
#include <svl/cryptosign.hxx>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Shlobj.h>
|
||||
|
@ -1922,7 +1923,9 @@ bool SfxStoringHelper::FinishGUIStoreModel(::comphelper::SequenceAsHashMap::cons
|
|||
{
|
||||
bFoundCert = true;
|
||||
SfxObjectShell* pDocShell = SfxViewShell::Current()->GetObjectShell();
|
||||
bool bSigned = pDocShell->SignDocumentContentUsingCertificate(xCert);
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
aSigningContext.m_xCertificate = xCert;
|
||||
bool bSigned = pDocShell->SignDocumentContentUsingCertificate(aSigningContext);
|
||||
if (bSigned && pDocShell->HasValidSignatures())
|
||||
{
|
||||
std::unique_ptr<weld::MessageDialog> xBox(
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
#include <comphelper/lok.hxx>
|
||||
#include <LibreOfficeKit/LibreOfficeKitEnums.h>
|
||||
#include <tools/link.hxx>
|
||||
#include <svl/cryptosign.hxx>
|
||||
|
||||
#include <sfx2/signaturestate.hxx>
|
||||
#include <sfx2/sfxresid.hxx>
|
||||
|
@ -579,7 +580,9 @@ void SfxObjectShell::ExecFile_Impl(SfxRequest &rReq)
|
|||
if (xCertificate.is())
|
||||
{
|
||||
|
||||
bHaveWeSigned |= SignDocumentContentUsingCertificate(xCertificate);
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
aSigningContext.m_xCertificate = xCertificate;
|
||||
bHaveWeSigned |= SignDocumentContentUsingCertificate(aSigningContext);
|
||||
|
||||
// Reload to show how the PDF actually looks like after signing. This also
|
||||
// changes "finish signing" on the infobar back to "sign document" as a side
|
||||
|
@ -2194,14 +2197,16 @@ bool SfxObjectShell::ResignDocument(uno::Sequence< security::DocumentSignatureIn
|
|||
auto xCert = rInfo.Signer;
|
||||
if (xCert.is())
|
||||
{
|
||||
bSignSuccess &= SignDocumentContentUsingCertificate(xCert);
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
aSigningContext.m_xCertificate = xCert;
|
||||
bSignSuccess &= SignDocumentContentUsingCertificate(aSigningContext);
|
||||
}
|
||||
}
|
||||
|
||||
return bSignSuccess;
|
||||
}
|
||||
|
||||
bool SfxObjectShell::SignDocumentContentUsingCertificate(const Reference<XCertificate>& xCertificate)
|
||||
bool SfxObjectShell::SignDocumentContentUsingCertificate(svl::crypto::SigningContext& rSigningContext)
|
||||
{
|
||||
// 1. PrepareForSigning
|
||||
|
||||
|
@ -2271,7 +2276,7 @@ bool SfxObjectShell::SignDocumentContentUsingCertificate(const Reference<XCertif
|
|||
|
||||
// 3. Sign
|
||||
bool bSignSuccess = GetMedium()->SignDocumentContentUsingCertificate(
|
||||
GetBaseModel(), HasValidSignatures(), xCertificate);
|
||||
GetBaseModel(), HasValidSignatures(), rSigningContext);
|
||||
|
||||
// 4. AfterSigning
|
||||
AfterSigning(bSignSuccess, false);
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
#include <comphelper/scopeguard.hxx>
|
||||
#include <comphelper/base64.hxx>
|
||||
#include <tools/json_writer.hxx>
|
||||
#include <svl/cryptosign.hxx>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
|
@ -992,6 +993,32 @@ void SfxLokHelper::addCertificates(const std::vector<std::string>& rCerts)
|
|||
pObjectShell->RecheckSignature(false);
|
||||
}
|
||||
|
||||
bool SfxLokHelper::supportsCommand(std::u16string_view rCommand)
|
||||
{
|
||||
static const std::initializer_list<std::u16string_view> vSupport = { u"Signature" };
|
||||
|
||||
return std::find(vSupport.begin(), vSupport.end(), rCommand) != vSupport.end();
|
||||
}
|
||||
|
||||
void SfxLokHelper::getCommandValues(tools::JsonWriter& rJsonWriter, std::string_view rCommand)
|
||||
{
|
||||
static constexpr OStringLiteral aSignature(".uno:Signature");
|
||||
if (!o3tl::starts_with(rCommand, aSignature))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SfxObjectShell* pObjectShell = SfxObjectShell::Current();
|
||||
if (!pObjectShell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
pObjectShell->SignDocumentContentUsingCertificate(aSigningContext);
|
||||
rJsonWriter.put("signatureTime", aSigningContext.m_nSignatureTime);
|
||||
}
|
||||
|
||||
void SfxLokHelper::notifyUpdate(SfxViewShell const* pThisView, int nType)
|
||||
{
|
||||
if (DisableCallbacks::disabled())
|
||||
|
|
|
@ -27,6 +27,7 @@ $(eval $(call gb_CppunitTest_use_libraries,vcl_filter_ipdf, \
|
|||
sal \
|
||||
sfx \
|
||||
subsequenttest \
|
||||
svl \
|
||||
svx \
|
||||
test \
|
||||
tl \
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include <sfx2/objsh.hxx>
|
||||
#include <vcl/filter/PDFiumLibrary.hxx>
|
||||
#include <vcl/filter/pdfdocument.hxx>
|
||||
#include <svl/cryptosign.hxx>
|
||||
|
||||
using namespace ::com::sun::star;
|
||||
|
||||
|
@ -109,7 +110,9 @@ CPPUNIT_TEST_FIXTURE(VclFilterIpdfTest, testPDFAddVisibleSignatureLastPage)
|
|||
pObjectShell->SetModified(false);
|
||||
|
||||
// When: do the actual signing.
|
||||
pObjectShell->SignDocumentContentUsingCertificate(xCert);
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
aSigningContext.m_xCertificate = xCert;
|
||||
pObjectShell->SignDocumentContentUsingCertificate(aSigningContext);
|
||||
|
||||
// Then: count the # of shapes on the signature widget/annotation.
|
||||
std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
|
||||
|
|
|
@ -22,6 +22,7 @@ $(eval $(call gb_CppunitTest_use_libraries,xmlsecurity_signing, \
|
|||
sal \
|
||||
sax \
|
||||
sfx \
|
||||
svl \
|
||||
svx \
|
||||
subsequenttest \
|
||||
test \
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
#include <comphelper/propertyvalue.hxx>
|
||||
#include <vcl/filter/PDFiumLibrary.hxx>
|
||||
#include <vcl/scheduler.hxx>
|
||||
#include <svl/cryptosign.hxx>
|
||||
|
||||
using namespace com::sun::star;
|
||||
|
||||
|
@ -765,7 +766,9 @@ CPPUNIT_TEST_FIXTURE(SigningTest, testPDFAddVisibleSignature)
|
|||
pObjectShell->SetModified(false);
|
||||
|
||||
// When: do the actual signing.
|
||||
pObjectShell->SignDocumentContentUsingCertificate(xCert);
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
aSigningContext.m_xCertificate = xCert;
|
||||
pObjectShell->SignDocumentContentUsingCertificate(aSigningContext);
|
||||
|
||||
// Then: count the # of shapes on the signature widget/annotation.
|
||||
std::unique_ptr<vcl::pdf::PDFiumDocument> pPdfDocument = parsePDFExport();
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
#include <com/sun/star/security/XDocumentDigitalSignatures.hpp>
|
||||
#include <com/sun/star/xml/crypto/XXMLSecurityContext.hpp>
|
||||
#include <sfx2/digitalsignatures.hxx>
|
||||
#include <svl/cryptosign.hxx>
|
||||
|
||||
#include <map>
|
||||
|
||||
|
@ -103,7 +104,7 @@ private:
|
|||
|
||||
bool
|
||||
signWithCertificateImpl(const uno::Reference<frame::XModel>& /*xModel*/,
|
||||
css::uno::Reference<css::security::XCertificate> const& xCertificate,
|
||||
svl::crypto::SigningContext& rSigningContext,
|
||||
css::uno::Reference<css::embed::XStorage> const& xStorage,
|
||||
css::uno::Reference<css::io::XStream> const& xStream,
|
||||
DocumentSignatureMode eMode);
|
||||
|
@ -190,7 +191,7 @@ public:
|
|||
/// See sfx2::DigitalSignatures::SignModelWithCertificate().
|
||||
bool
|
||||
SignModelWithCertificate(const css::uno::Reference<css::frame::XModel>& xModel,
|
||||
const css::uno::Reference<css::security::XCertificate>& xCertificate,
|
||||
svl::crypto::SigningContext& rSigningContext,
|
||||
const css::uno::Reference<css::embed::XStorage>& xStorage,
|
||||
const css::uno::Reference<css::io::XStream>& xStream) override;
|
||||
/// See sfx2::DigitalSignatures::SignDocumentContentAsync().
|
||||
|
@ -759,17 +760,19 @@ sal_Bool DocumentDigitalSignatures::signDocumentWithCertificate(
|
|||
css::uno::Reference<css::io::XStream> const & xStream)
|
||||
{
|
||||
uno::Reference<frame::XModel> xModel;
|
||||
return signWithCertificateImpl(xModel, xCertificate, xStorage, xStream,
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
aSigningContext.m_xCertificate = xCertificate;
|
||||
return signWithCertificateImpl(xModel, aSigningContext, xStorage, xStream,
|
||||
DocumentSignatureMode::Content);
|
||||
}
|
||||
|
||||
bool DocumentDigitalSignatures::SignModelWithCertificate(
|
||||
const uno::Reference<frame::XModel>& xModel,
|
||||
const css::uno::Reference<css::security::XCertificate>& xCertificate,
|
||||
svl::crypto::SigningContext& rSigningContext,
|
||||
const css::uno::Reference<css::embed::XStorage>& xStorage,
|
||||
const css::uno::Reference<css::io::XStream>& xStream)
|
||||
{
|
||||
return signWithCertificateImpl(xModel, xCertificate, xStorage, xStream,
|
||||
return signWithCertificateImpl(xModel, rSigningContext, xStorage, xStream,
|
||||
DocumentSignatureMode::Content);
|
||||
}
|
||||
|
||||
|
@ -814,13 +817,15 @@ sal_Bool DocumentDigitalSignatures::signScriptingContentWithCertificate(
|
|||
css::uno::Reference<css::io::XStream> const& xStream)
|
||||
{
|
||||
uno::Reference<frame::XModel> xModel;
|
||||
return signWithCertificateImpl(xModel, xCertificate, xStorage, xStream,
|
||||
svl::crypto::SigningContext aSigningContext;
|
||||
aSigningContext.m_xCertificate = xCertificate;
|
||||
return signWithCertificateImpl(xModel, aSigningContext, xStorage, xStream,
|
||||
DocumentSignatureMode::Macros);
|
||||
}
|
||||
|
||||
bool DocumentDigitalSignatures::signWithCertificateImpl(
|
||||
const uno::Reference<frame::XModel>& xModel,
|
||||
css::uno::Reference<css::security::XCertificate> const& xCertificate,
|
||||
svl::crypto::SigningContext& rSigningContext,
|
||||
css::uno::Reference<css::embed::XStorage> const& xStorage,
|
||||
css::uno::Reference<css::io::XStream> const& xStream, DocumentSignatureMode eMode)
|
||||
{
|
||||
|
@ -838,8 +843,8 @@ bool DocumentDigitalSignatures::signWithCertificateImpl(
|
|||
aSignatureManager.setModel(xModel);
|
||||
|
||||
Reference<XXMLSecurityContext> xSecurityContext;
|
||||
Reference<XServiceInfo> xServiceInfo(xCertificate, UNO_QUERY);
|
||||
if (xServiceInfo->getImplementationName()
|
||||
Reference<XServiceInfo> xServiceInfo(rSigningContext.m_xCertificate, UNO_QUERY);
|
||||
if (xServiceInfo.is() && xServiceInfo->getImplementationName()
|
||||
== "com.sun.star.xml.security.gpg.XCertificate_GpgImpl")
|
||||
xSecurityContext = aSignatureManager.getGpgSecurityContext();
|
||||
else
|
||||
|
@ -847,7 +852,7 @@ bool DocumentDigitalSignatures::signWithCertificateImpl(
|
|||
|
||||
sal_Int32 nSecurityId;
|
||||
|
||||
bool bSuccess = aSignatureManager.add(xCertificate, xSecurityContext, u""_ustr, nSecurityId, true);
|
||||
bool bSuccess = aSignatureManager.add(rSigningContext.m_xCertificate, xSecurityContext, u""_ustr, nSecurityId, true);
|
||||
if (!bSuccess)
|
||||
return false;
|
||||
|
||||
|
|
Loading…
Reference in a new issue