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