From c00a23393a120fdbaf2d16f3ef45ff39b9fe3f1e Mon Sep 17 00:00:00 2001 From: Balazs Varga Date: Tue, 5 May 2026 10:17:26 +0200 Subject: [PATCH] tdf#157943 embeddedobj: refuse OLE-link edit when source is in use After reverting 2b4cd99d, editing an OLE link whose source is already open in this LO instance produces a sharing-violation on save (the intra-process scenario tdf#126742 was working around). Detect the conflict at doVerb, gated to verbs targeting a visible state so internal RUNNING transitions aren't intercepted. If the source is loaded as another visible frame in this LO, switch to that window (matches MSO behaviour). Otherwise probe for any .~lock or MSO ~$ file - foreign user, hidden own frame, or stale own lock from a crash - and show an async warning with the lock owner. In all cases the OLE working copy is never created. Skipped in headless mode. Change-Id: Ic87bb6f073b0b04ec47597cc15f2c5a83cc2bffc Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/2004 Reviewed-by: Michael Stahl Tested-by: Jenkins CPCI Reviewed-on: https://gerrit.libreoffice.org/c/core/+/204940 Reviewed-by: Balazs Varga Tested-by: Jenkins --- embeddedobj/Library_embobj.mk | 1 + .../source/commonembedding/embedobj.cxx | 238 +++++++++++++++++- embeddedobj/source/inc/strings.hrc | 1 + 3 files changed, 239 insertions(+), 1 deletion(-) diff --git a/embeddedobj/Library_embobj.mk b/embeddedobj/Library_embobj.mk index 18cf55518d44..03c96f79b341 100644 --- a/embeddedobj/Library_embobj.mk +++ b/embeddedobj/Library_embobj.mk @@ -34,6 +34,7 @@ $(eval $(call gb_Library_use_libraries,embobj,\ cppu \ cppuhelper \ sal \ + svl \ svt \ utl \ vcl \ diff --git a/embeddedobj/source/commonembedding/embedobj.cxx b/embeddedobj/source/commonembedding/embedobj.cxx index a5ce1443515e..a5e778065325 100644 --- a/embeddedobj/source/commonembedding/embedobj.cxx +++ b/embeddedobj/source/commonembedding/embedobj.cxx @@ -17,6 +17,8 @@ * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ +#include + #include #include #include @@ -41,9 +43,25 @@ #include #include #include - #include +#if HAVE_FEATURE_MULTIUSER_ENVIRONMENT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + #include #include @@ -51,6 +69,180 @@ #include #include +#if HAVE_FEATURE_MULTIUSER_ENVIRONMENT +namespace { + +// tdf#157943 / tdf#126742: locate the visible top-level frame loaded from +// sLinkURL, or null. Hidden frames are skipped (caller routes them to the +// warn path). Comparison mirrors LoadEnv::impl_searchAlreadyLoaded. +css::uno::Reference< css::frame::XFrame > findLinkSourceFrame( + const css::uno::Reference< css::uno::XComponentContext >& xContext, + const OUString& sLinkURL ) +{ + if ( sLinkURL.isEmpty() || INetURLObject( sLinkURL ).IsExoticProtocol() ) + return nullptr; + + try + { + css::uno::Reference< css::frame::XDesktop2 > xDesktop = css::frame::Desktop::create( xContext ); + css::uno::Reference< css::container::XIndexAccess > xFrames = xDesktop->getFrames(); + if ( !xFrames.is() ) + return nullptr; + + const sal_Int32 nCount = xFrames->getCount(); + for ( sal_Int32 i = 0; i < nCount; ++i ) + { + try + { + css::uno::Reference< css::frame::XFrame > xFrame; + xFrames->getByIndex( i ) >>= xFrame; + if ( !xFrame.is() ) + continue; + + OUString sFrameURL; + css::uno::Reference< css::frame::XController > xController = xFrame->getController(); + if ( xController.is() ) + { + css::uno::Reference< css::frame::XModel > xModel = xController->getModel(); + if ( xModel.is() ) + { + // Skip hidden frames. Calling activate() / toFront() + // on one would either silently no-op or unexpectedly + // reveal a frame meant to stay invisible. + bool bHidden = false; + for ( const auto& rProp : xModel->getArgs() ) + { + if ( rProp.Name == "Hidden" ) + { + rProp.Value >>= bHidden; + break; + } + } + if ( bHidden ) + continue; + + sFrameURL = xModel->getURL(); + } + } + else + { + // load may be in progress - URL lives on the frame itself + css::uno::Reference< css::beans::XPropertySet > xFrameProps( xFrame, css::uno::UNO_QUERY ); + if ( xFrameProps.is() ) + xFrameProps->getPropertyValue( u"URL"_ustr ) >>= sFrameURL; + } + + if ( sFrameURL.isEmpty() ) + continue; + + if ( ::utl::UCBContentHelper::EqualURLs( sLinkURL, sFrameURL ) ) + return xFrame; + } + catch ( const css::uno::Exception& ) + { + // ignore individual frame errors and keep iterating + } + } + } + catch ( const css::uno::Exception& ) + { + } + return nullptr; +} + +// Bring an already-loaded source document's frame to the front. Best-effort: +// failure here is harmless because the caller refuses the OLE activation +// regardless, which is the safe outcome. +void switchToExistingFrame( const css::uno::Reference< css::frame::XFrame >& xFrame ) +{ + if ( !xFrame.is() ) + return; + + try + { + xFrame->activate(); + css::uno::Reference< css::awt::XTopWindow > xTopWindow( xFrame->getContainerWindow(), css::uno::UNO_QUERY ); + if ( xTopWindow.is() ) + xTopWindow->toFront(); + } + catch ( const css::uno::Exception& ) + { + } +} + +// tdf#157943: any lock - foreign, hidden own frame, or stale own lock - +// would fail the OLE writeback at save time, so the caller refuses on +// any non-empty result. Visible-own-frame is filtered out earlier. +OUString getSourceLockOwner( std::u16string_view sLinkURL ) +{ + if ( sLinkURL.empty() || INetURLObject( sLinkURL ).IsExoticProtocol() ) + return u""_ustr; + + auto formatOwner = []( const LockFileEntry& aData, bool bMSO ) + { + OUString sOwner = aData[LockFileComponent::OOOUSERNAME]; + if ( sOwner.isEmpty() ) + sOwner = aData[LockFileComponent::SYSUSERNAME]; + if ( sOwner.isEmpty() ) + return u""_ustr; // empty/corrupt lock data - treat as no lock + if ( bMSO ) + sOwner += " (MS Office)"; + return sOwner; + }; + + // LO format (.~lock.*#) + try + { + svt::DocumentLockFile aLockFile( sLinkURL ); + if ( OUString sOwner = formatOwner( aLockFile.GetLockData(), false ); + !sOwner.isEmpty() ) + return sOwner; + } + catch ( const css::uno::Exception& ) + { + } + + // MSO format (~$*) - LO understands these via tdf#34171 + if ( svt::MSODocumentLockFile::IsMSOSupportedFileFormat( sLinkURL ) ) + { + try + { + svt::MSODocumentLockFile aMSOLockFile( sLinkURL ); + if ( OUString sOwner = formatOwner( aMSOLockFile.GetLockData(), true ); + !sOwner.isEmpty() ) + return sOwner; + } + catch ( const css::uno::Exception& ) + { + } + } + + return u""_ustr; +} + +void showLinkSourceLockedDialog( const css::uno::Reference< css::awt::XWindow >& xClientWindow, + std::u16string_view sLinkURL, + std::u16string_view sOwner ) +{ + std::locale aResLocale = Translate::Create( "emo" ); + OUString aMsg = Translate::get( STR_LINK_SOURCE_LOCKED, aResLocale ); + + OUString aLinkURL( sLinkURL ); + OUString aSysPath = aLinkURL; + osl::FileBase::getSystemPathFromFileURL( aLinkURL, aSysPath ); + aMsg = aMsg.replaceFirst( "%{filename}", aSysPath ); + aMsg = aMsg.replaceFirst( "%{owner}", sOwner ); + + weld::Window* pParent = Application::GetFrameWeld( xClientWindow ); + std::shared_ptr< weld::MessageDialog > xQueryBox( + Application::CreateMessageDialog( pParent, + VclMessageType::Warning, VclButtonsType::Ok, aMsg ) ); + xQueryBox->runAsync( xQueryBox, []( sal_Int32 ) {} ); +} + +} // anonymous namespace +#endif + using namespace ::com::sun::star; awt::Rectangle GetRectangleInterception( const awt::Rectangle& aRect1, const awt::Rectangle& aRect2 ) @@ -603,7 +795,51 @@ void SAL_CALL OCommonEmbeddedObject::doVerb( sal_Int32 nVerbID ) } else { +#if HAVE_FEATURE_MULTIUSER_ENVIRONMENT + // tdf#157943 / tdf#126742: refuse user-initiated activation of an + // OLE link whose source is in use - either navigate to the visible + // frame (matches MSO) or warn on any lock (foreign, hidden own + // frame, or stale). Gated to visible-state verbs so internal + // RUNNING transitions aren't redirected. Skipped in headless. + if ( m_bIsLinkURL + && ( nNewState == embed::EmbedStates::INPLACE_ACTIVE + || nNewState == embed::EmbedStates::UI_ACTIVE + || nNewState == embed::EmbedStates::ACTIVE ) + && !Application::IsHeadlessModeEnabled() ) + { + // snapshot under m_aMutex so the framework calls below run on + // stable values even if breakLink/reload races on another thread + const OUString aLinkURL = m_aLinkURL; + const auto xClientWindow = m_xClientWindow; + const auto xContext = m_xContext; + aGuard.clear(); + + // 1. Source loaded as a visible frame here: bring it to front. + // Matches MSO's "navigate to existing editor" behaviour. + if ( auto xExistingFrame = findLinkSourceFrame( xContext, aLinkURL ); + xExistingFrame.is() ) + { + switchToExistingFrame( xExistingFrame ); + return; + } + + // 2. Lock file present (foreign, hidden own frame, or stale + // own lock from a crash) - all fail writeback at save time. + if ( OUString sLockOwner = getSourceLockOwner( aLinkURL ); + !sLockOwner.isEmpty() ) + { + showLinkSourceLockedDialog( xClientWindow, aLinkURL, sLockOwner ); + return; + } + } + else + { + aGuard.clear(); + } +#else aGuard.clear(); +#endif + changeState( nNewState ); } } diff --git a/embeddedobj/source/inc/strings.hrc b/embeddedobj/source/inc/strings.hrc index 420d2d05d091..2936b88d577d 100644 --- a/embeddedobj/source/inc/strings.hrc +++ b/embeddedobj/source/inc/strings.hrc @@ -14,6 +14,7 @@ #define BTN_OVERWRITE_TEXT NC_("BTN_OVERWRITE_TEXT", "Overwrite") #define STR_OVERWRITE_LINK NC_("STR_OVERWRITE_LINK", "You have made changes to the %{filename}, saving will overwrite the data from the inserted object.\n\nDo you still want to overwrite this data?") #define STR_OVERWRITE_TEMP NC_("STR_OVERWRITE_TEMP", "You have changed the data in the inserted object which will be overwritten by updating the %{filename}.\n\nDo you still want to overwrite this data?") +#define STR_LINK_SOURCE_LOCKED NC_("STR_LINK_SOURCE_LOCKED", "The source of this OLE link, %{filename}, is currently locked by %{owner}.\n\nSaving changes through this link will fail. Please close the file before editing.") /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */