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: */

Reply via email to