sw/CppunitTest_sw_embedded_fonts.mk | 78 ++ sw/Module_sw.mk | 1 sw/qa/extras/embedded_fonts/data/embed-restricted+unrestricted.docx |binary sw/qa/extras/embedded_fonts/data/embed-restricted-style+autostyle.odt |binary sw/qa/extras/embedded_fonts/embedded_fonts.cxx | 289 ++++++++++ 5 files changed, 368 insertions(+)
New commits: commit 5f40ebefdc04869810fa8388871f9b98e4659d11 Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Tue Aug 12 20:17:53 2025 +0500 Commit: Miklos Vajna <vmik...@collabora.com> CommitDate: Fri Aug 15 09:00:24 2025 +0200 Loading documents with restricted fonts: add unit tests Change-Id: I04e4bad97f1fe19532fa1d896774567fb82d1b68 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/189439 Tested-by: Jenkins Reviewed-by: Mike Kaganski <mike.kagan...@collabora.com> (cherry picked from commit 4e92716130ad8c2bfdbb83605dd7a66182af1faa) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/189449 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Miklos Vajna <vmik...@collabora.com> diff --git a/sw/CppunitTest_sw_embedded_fonts.mk b/sw/CppunitTest_sw_embedded_fonts.mk new file mode 100644 index 000000000000..93a3adf17e69 --- /dev/null +++ b/sw/CppunitTest_sw_embedded_fonts.mk @@ -0,0 +1,78 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t; 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/. +# + +$(eval $(call gb_CppunitTest_CppunitTest,sw_embedded_fonts)) + +$(eval $(call gb_CppunitTest_use_common_precompiled_header,sw_embedded_fonts)) + +$(eval $(call gb_CppunitTest_add_exception_objects,sw_embedded_fonts, \ + sw/qa/extras/embedded_fonts/embedded_fonts \ +)) + +$(eval $(call gb_CppunitTest_use_libraries,sw_embedded_fonts, \ + comphelper \ + editeng \ + cppu \ + cppuhelper \ + sal \ + svt \ + sfx \ + subsequenttest \ + svl \ + sw \ + swqahelper \ + test \ + unotest \ + vcl \ + tl \ + utl \ +)) + +$(eval $(call gb_CppunitTest_use_externals,sw_embedded_fonts,\ + boost_headers \ + libxml2 \ +)) + +$(eval $(call gb_CppunitTest_set_include,sw_embedded_fonts,\ + -I$(SRCDIR)/sw/inc \ + -I$(SRCDIR)/sw/source/core/inc \ + -I$(SRCDIR)/sw/source/uibase/inc \ + -I$(SRCDIR)/sw/qa/inc \ + $$(INCLUDE) \ +)) + +$(eval $(call gb_CppunitTest_use_system_win32_libs,sw_embedded_fonts,\ + ole32 \ +)) + +$(eval $(call gb_CppunitTest_use_api,sw_embedded_fonts,\ + udkapi \ + offapi \ + oovbaapi \ +)) + +$(eval $(call gb_CppunitTest_use_ure,sw_embedded_fonts)) +$(eval $(call gb_CppunitTest_use_vcl,sw_embedded_fonts)) + +$(eval $(call gb_CppunitTest_use_rdb,sw_embedded_fonts,services)) + +$(eval $(call gb_CppunitTest_use_configuration,sw_embedded_fonts)) + +$(eval $(call gb_CppunitTest_add_arguments,sw_embedded_fonts, \ + -env:arg-env=$(gb_Helper_LIBRARY_PATH_VAR)"$$$${$(gb_Helper_LIBRARY_PATH_VAR)+=$$$$$(gb_Helper_LIBRARY_PATH_VAR)}" \ +)) + +$(eval $(call gb_CppunitTest_use_custom_headers,sw_embedded_fonts,\ + officecfg/registry \ +)) + +# Explicitly allow non-application fonts +$(eval $(call gb_CppunitTest_set_non_application_font_use,sw_embedded_fonts,allow)) + +# vim: set noet sw=4 ts=4: diff --git a/sw/Module_sw.mk b/sw/Module_sw.mk index aa4d5fe5de3c..72339260e2da 100644 --- a/sw/Module_sw.mk +++ b/sw/Module_sw.mk @@ -118,6 +118,7 @@ $(eval $(call gb_Module_add_slowcheck_targets,sw,\ CppunitTest_sw_odfexport \ CppunitTest_sw_odfexport2 \ CppunitTest_sw_odfimport \ + CppunitTest_sw_embedded_fonts \ CppunitTest_sw_txtexport \ CppunitTest_sw_txtencexport \ CppunitTest_sw_txtimport \ diff --git a/sw/qa/extras/embedded_fonts/data/embed-restricted+unrestricted.docx b/sw/qa/extras/embedded_fonts/data/embed-restricted+unrestricted.docx new file mode 100644 index 000000000000..1e17c8a0c35f Binary files /dev/null and b/sw/qa/extras/embedded_fonts/data/embed-restricted+unrestricted.docx differ diff --git a/sw/qa/extras/embedded_fonts/data/embed-restricted-style+autostyle.odt b/sw/qa/extras/embedded_fonts/data/embed-restricted-style+autostyle.odt new file mode 100644 index 000000000000..3e694b2eedd4 Binary files /dev/null and b/sw/qa/extras/embedded_fonts/data/embed-restricted-style+autostyle.odt differ diff --git a/sw/qa/extras/embedded_fonts/embedded_fonts.cxx b/sw/qa/extras/embedded_fonts/embedded_fonts.cxx new file mode 100644 index 000000000000..4cb5e261e6ab --- /dev/null +++ b/sw/qa/extras/embedded_fonts/embedded_fonts.cxx @@ -0,0 +1,289 @@ +/* -*- 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 <sal/config.h> + +#include <config_features.h> + +#include <swmodeltestbase.hxx> + +#include <com/sun/star/awt/Toolkit.hpp> +#include <com/sun/star/awt/XFontMappingUse.hpp> +#include <com/sun/star/document/FontsDisallowEditingRequest.hpp> +#include <com/sun/star/task/XInteractionApprove.hpp> +#include <com/sun/star/task/XInteractionDisapprove.hpp> +#include <com/sun/star/task/XInteractionHandler.hpp> + +#include <comphelper/compbase.hxx> +#include <comphelper/processfactory.hxx> +#include <comphelper/propertyvalue.hxx> +#include <rtl/ref.hxx> + +#include <docsh.hxx> + +namespace +{ +class Test : public SwModelTestBase +{ +public: + Test() + : SwModelTestBase(u"/sw/qa/extras/embedded_fonts/data/"_ustr, u"writer8"_ustr) + { + } +}; + +class FontInteractionHandler : public comphelper::WeakImplHelper<task::XInteractionHandler> +{ +public: + FontInteractionHandler(bool bApprove) + : mbApprove(bApprove) + { + } + + int getRequestCount() const { return mnRequestCount; } + OUString getRequestedFontName() const { return maRequestedFontName; } + + virtual void SAL_CALL handle(uno::Reference<task::XInteractionRequest> const& rRequest) override + { + const auto aContinuations = rRequest->getContinuations(); + + if (handleRestrictedFontRequest(aContinuations, rRequest->getRequest())) + return; + + for (auto const& continuation : aContinuations) + { + if (auto xApprove = continuation.query<task::XInteractionApprove>()) + { + xApprove->select(); + break; + } + } + } + + bool handleRestrictedFontRequest( + const uno::Sequence<uno::Reference<task::XInteractionContinuation>>& rContinuations, + const uno::Any& rRequest) + { + document::FontsDisallowEditingRequest aRequest; + if (!(rRequest >>= aRequest)) + return false; + + ++mnRequestCount; + maRequestedFontName += aRequest.aFontNames; + + for (auto const& continuation : rContinuations) + { + if (mbApprove) + { + if (auto xApprove = continuation.query<task::XInteractionApprove>()) + { + xApprove->select(); + break; + } + } + else + { + if (auto xDisapprove = continuation.query<task::XInteractionDisapprove>()) + { + xDisapprove->select(); + break; + } + } + } + return true; + } + +private: + bool mbApprove; // Approve the request or not + int mnRequestCount = 0; // How many times had restricted font request happened + OUString maRequestedFontName; +}; + +class FontMappingUseListener +{ +public: + FontMappingUseListener() + : mxFontMappingUse(awt::Toolkit::create(comphelper::getProcessComponentContext()) + .queryThrow<awt::XFontMappingUse>()) + { + mxFontMappingUse->startTrackingFontMappingUse(); + } + ~FontMappingUseListener() { mxFontMappingUse->finishTrackingFontMappingUse(); } + + void checkpoint() + { + maFontMappingUseData = mxFontMappingUse->finishTrackingFontMappingUse(); + mxFontMappingUse->startTrackingFontMappingUse(); + } + + bool wasUsed(std::u16string_view font) const + { + for (const auto& element : maFontMappingUseData) + { + if (element.originalFont == font) + return true; + } + return false; + } + + bool wasSubstituted(std::u16string_view font) const + { + for (const auto& element : maFontMappingUseData) + { + if (element.originalFont != font) + continue; + + for (const auto& used : element.usedFonts) + { + std::u16string_view rest; + if (!used.startsWith(element.originalFont, &rest) + || (!rest.empty() && !rest.starts_with('/'))) + return true; + } + } + return false; + } + +private: + uno::Reference<awt::XFontMappingUse> mxFontMappingUse; + uno::Sequence<awt::XFontMappingUseItem> maFontMappingUseData; +}; + +CPPUNIT_TEST_FIXTURE(Test, testOpenODTWithRestrictedEmbeddedFont) +{ + // The ODT has a restricted embedded font, referenced both from styles.xml and content.xml. + // Test its loading without and with approval; and check that there are no double requests + { + // 1. Load and do not approve the restricted font + FontMappingUseListener fontMappingData; + rtl::Reference xInteraction(new FontInteractionHandler(false)); + loadWithParams(createFileURL(u"embed-restricted-style+autostyle.odt"), + { comphelper::makePropertyValue( + u"InteractionHandler"_ustr, + uno::Reference<task::XInteractionHandler>(xInteraction)) }); + + // It asked exactly once, even though both styles.xml and content.xml requested the font: + CPPUNIT_ASSERT_EQUAL(1, xInteraction->getRequestCount()); + // It requested the expected font + CPPUNIT_ASSERT_EQUAL(u"Naftalene"_ustr, xInteraction->getRequestedFontName().trim()); + // The document is editable: + CPPUNIT_ASSERT(!getSwDocShell()->IsReadOnly()); + + fontMappingData.checkpoint(); + // The request was disapproved, and the font didn't load; so it was substituted: + CPPUNIT_ASSERT(fontMappingData.wasUsed(u"Naftalene")); + CPPUNIT_ASSERT(fontMappingData.wasSubstituted(u"Naftalene")); + } + + { + // 2. Load and approve the restricted font + FontMappingUseListener fontMappingData; + rtl::Reference xInteraction(new FontInteractionHandler(true)); + loadWithParams(createFileURL(u"embed-restricted-style+autostyle.odt"), + { comphelper::makePropertyValue( + u"InteractionHandler"_ustr, + uno::Reference<task::XInteractionHandler>(xInteraction)) }); + + // It asked exactly once, even though both styles.xml and content.xml requested the font: + CPPUNIT_ASSERT_EQUAL(1, xInteraction->getRequestCount()); + // It requested the expected font + CPPUNIT_ASSERT_EQUAL(u"Naftalene"_ustr, xInteraction->getRequestedFontName().trim()); + // The document loaded read-only: + CPPUNIT_ASSERT(getSwDocShell()->IsReadOnly()); + + fontMappingData.checkpoint(); + // The request was approved, and the font loaded; no substitution happened: + CPPUNIT_ASSERT(fontMappingData.wasUsed(u"Naftalene")); + CPPUNIT_ASSERT(!fontMappingData.wasSubstituted(u"Naftalene")); + } +} + +CPPUNIT_TEST_FIXTURE(Test, testOpenDOCXWithRestrictedEmbeddedFont) +{ + // The DOCX has two embedded fonts, one restricted (Naftalene), one unrestricted (Unsteady + // Oversteer). Test without interaction handler, and with handler (without and with approval). + { + // 1. Load without interaction handler. It must not load the restricted font; + // unrestricted one must load. + FontMappingUseListener fontMappingData; + loadWithParams(createFileURL(u"embed-restricted+unrestricted.docx"), {}); + + // The document is editable: + CPPUNIT_ASSERT(!getSwDocShell()->IsReadOnly()); + + fontMappingData.checkpoint(); + + // Interaction handler was absent, and the restricted font didn't load; it was substituted: + CPPUNIT_ASSERT(fontMappingData.wasUsed(u"Naftalene")); + CPPUNIT_ASSERT(fontMappingData.wasSubstituted(u"Naftalene")); + + // Unrestricted font was loaded and used without substitution: + CPPUNIT_ASSERT(fontMappingData.wasUsed(u"Unsteady Oversteer")); + CPPUNIT_ASSERT(!fontMappingData.wasSubstituted(u"Unsteady Oversteer")); + } + + { + // 2. Load and do not approve the restricted font. It must not load the restricted font; + // unrestricted one must load. + FontMappingUseListener fontMappingData; + rtl::Reference xInteraction(new FontInteractionHandler(false)); + loadWithParams(createFileURL(u"embed-restricted+unrestricted.docx"), + { comphelper::makePropertyValue( + u"InteractionHandler"_ustr, + uno::Reference<task::XInteractionHandler>(xInteraction)) }); + + CPPUNIT_ASSERT_EQUAL(1, xInteraction->getRequestCount()); + // It requested only the expected font (no requests for 'Unsteady Oversteer') + CPPUNIT_ASSERT_EQUAL(u"Naftalene"_ustr, xInteraction->getRequestedFontName().trim()); + // The document is editable: + CPPUNIT_ASSERT(!getSwDocShell()->IsReadOnly()); + + fontMappingData.checkpoint(); + + // The request was disapproved, and the font didn't load; so it was substituted: + CPPUNIT_ASSERT(fontMappingData.wasUsed(u"Naftalene")); + CPPUNIT_ASSERT(fontMappingData.wasSubstituted(u"Naftalene")); + + // Unrestricted font was loaded and used without substitution: + CPPUNIT_ASSERT(fontMappingData.wasUsed(u"Unsteady Oversteer")); + CPPUNIT_ASSERT(!fontMappingData.wasSubstituted(u"Unsteady Oversteer")); + } + + { + // 3. Load and approve the restricted font. It must load both fonts, and open in read-only + // mode. + FontMappingUseListener fontMappingData; + rtl::Reference xInteraction(new FontInteractionHandler(true)); + loadWithParams(createFileURL(u"embed-restricted+unrestricted.docx"), + { comphelper::makePropertyValue( + u"InteractionHandler"_ustr, + uno::Reference<task::XInteractionHandler>(xInteraction)) }); + + CPPUNIT_ASSERT_EQUAL(1, xInteraction->getRequestCount()); + // It requested the expected font + CPPUNIT_ASSERT_EQUAL(u"Naftalene"_ustr, xInteraction->getRequestedFontName().trim()); + // The document loaded read-only: + CPPUNIT_ASSERT(getSwDocShell()->IsReadOnly()); + + fontMappingData.checkpoint(); + + // The request was approved, and the restricted font loaded; no substitution: + CPPUNIT_ASSERT(fontMappingData.wasUsed(u"Naftalene")); + CPPUNIT_ASSERT(!fontMappingData.wasSubstituted(u"Naftalene")); + + // Unrestricted font was loaded and used without substitution: + CPPUNIT_ASSERT(fontMappingData.wasUsed(u"Unsteady Oversteer")); + CPPUNIT_ASSERT(!fontMappingData.wasSubstituted(u"Unsteady Oversteer")); + } +} + +} // end of anonymous namespace +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */