diff options
Diffstat (limited to 'desktop/source/app')
25 files changed, 9423 insertions, 0 deletions
diff --git a/desktop/source/app/app.cxx b/desktop/source/app/app.cxx new file mode 100644 index 000000000..b205d5b18 --- /dev/null +++ b/desktop/source/app/app.cxx @@ -0,0 +1,2588 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <memory> +#include <config_features.h> +#include <config_feature_desktop.h> +#include <config_feature_opencl.h> +#include <config_java.h> +#include <config_folders.h> +#include <config_extensions.h> +#include <config_wasm_strip.h> + +#include <sal/config.h> + +#include <cstdlib> +#include <iostream> +#include <string_view> + +#include <app.hxx> +#include <dp_shared.hxx> +#include <strings.hrc> +#include "cmdlineargs.hxx" +#include <lockfile.hxx> +#include "userinstall.hxx" +#include "desktopcontext.hxx" +#include <migration.hxx> +#include "officeipcthread.hxx" +#if HAVE_FEATURE_UPDATE_MAR +#include "updater.hxx" +#endif + +#include <framework/desktop.hxx> +#include <i18nlangtag/languagetag.hxx> +#include <o3tl/char16_t2wchar_t.hxx> +#include <svl/ctloptions.hxx> +#include <svtools/javacontext.hxx> +#include <com/sun/star/beans/XPropertySet.hpp> +#include <com/sun/star/frame/theAutoRecovery.hpp> +#include <com/sun/star/frame/theGlobalEventBroadcaster.hpp> +#include <com/sun/star/frame/SessionListener.hpp> +#include <com/sun/star/frame/XSynchronousDispatch.hpp> +#include <com/sun/star/configuration/theDefaultProvider.hpp> +#include <com/sun/star/util/XFlushable.hpp> +#include <com/sun/star/system/SystemShellExecuteFlags.hpp> +#include <com/sun/star/frame/Desktop.hpp> +#include <com/sun/star/frame/StartModule.hpp> +#include <com/sun/star/view/XPrintable.hpp> +#include <com/sun/star/awt/XTopWindow.hpp> +#include <com/sun/star/util/URLTransformer.hpp> +#include <com/sun/star/util/XURLTransformer.hpp> +#include <com/sun/star/lang/ServiceNotRegisteredException.hpp> +#include <com/sun/star/configuration/MissingBootstrapFileException.hpp> +#include <com/sun/star/configuration/InvalidBootstrapFileException.hpp> +#include <com/sun/star/configuration/InstallationIncompleteException.hpp> +#include <com/sun/star/configuration/backend/BackendSetupException.hpp> +#include <com/sun/star/configuration/backend/BackendAccessException.hpp> +#include <com/sun/star/task/theJobExecutor.hpp> +#include <com/sun/star/task/OfficeRestartManager.hpp> +#include <com/sun/star/task/XRestartManager.hpp> +#include <com/sun/star/document/XDocumentEventListener.hpp> +#include <com/sun/star/office/Quickstart.hpp> +#include <com/sun/star/system/XSystemShellExecute.hpp> +#include <com/sun/star/system/SystemShellExecute.hpp> +#include <com/sun/star/loader/XImplementationLoader.hpp> + +#include <desktop/exithelper.h> +#include <sal/log.hxx> +#include <toolkit/helper/vclunohelper.hxx> +#include <comphelper/configuration.hxx> +#include <comphelper/fileurl.hxx> +#include <comphelper/threadpool.hxx> +#include <comphelper/processfactory.hxx> +#include <comphelper/backupfilehelper.hxx> +#include <uno/current_context.hxx> +#include <unotools/bootstrap.hxx> +#include <unotools/configmgr.hxx> +#include <unotools/moduleoptions.hxx> +#include <unotools/localfilehelper.hxx> +#include <unotools/ucbhelper.hxx> +#include <officecfg/Office/Common.hxx> +#include <officecfg/Office/Recovery.hxx> +#include <officecfg/Office/Update.hxx> +#include <officecfg/Setup.hxx> +#include <osl/file.hxx> +#include <osl/process.h> +#include <rtl/byteseq.hxx> +#include <unotools/pathoptions.hxx> +#if !ENABLE_WASM_STRIP_PINGUSER +#include <unotools/VersionConfig.hxx> +#endif +#include <rtl/bootstrap.hxx> +#include <vcl/test/GraphicsRenderTests.hxx> +#include <vcl/help.hxx> +#include <vcl/weld.hxx> +#include <vcl/settings.hxx> +#include <sfx2/flatpak.hxx> +#include <sfx2/sfxsids.hrc> +#include <sfx2/app.hxx> +#include <sfx2/safemode.hxx> +#include <svl/itemset.hxx> +#include <svl/eitem.hxx> +#include <basic/sbstar.hxx> +#include <desktop/crashreport.hxx> +#include <tools/urlobj.hxx> +#include <tools/diagnose_ex.h> +#include <svtools/fontsubstconfig.hxx> +#include <svtools/accessibilityoptions.hxx> +#include <svtools/apearcfg.hxx> +#include <vcl/graphicfilter.hxx> +#include <vcl/window.hxx> +#include "langselect.hxx" +#include <salhelper/thread.hxx> + +#if defined MACOSX +#include <errno.h> +#include <sys/wait.h> +#endif + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#include <vcl/fileregistration.hxx> +#endif + +#if defined(_WIN32) +#include <process.h> +#define GETPID _getpid +#else +#include <unistd.h> +#define GETPID getpid +#endif + +#include <strings.hxx> + +using namespace ::com::sun::star::awt; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::util; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::frame; +using namespace ::com::sun::star::document; +using namespace ::com::sun::star::view; +using namespace ::com::sun::star::task; +using namespace ::com::sun::star::system; +using namespace ::com::sun::star::ui; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::container; + +namespace desktop +{ + +static oslSignalHandler pSignalHandler = nullptr; + +namespace { + +#if HAVE_FEATURE_EXTENSIONS + +// Remove any existing UserInstallation's extensions cache data remaining from +// old installations. This addresses at least two problems: +// +// For one, apparently due to the old share/prereg/bundled mechanism (disabled +// since 5c47e5f63a79a9e72ec4a100786b1bbf65137ed4 "fdo#51252 Disable copying +// share/prereg/bundled to avoid startup crashes"), the user/extensions/bundled +// cache could contain corrupted information (like a UNO component registered +// twice, which got changed from active to passive registration in one LO +// version, but the version of the corresponding bundled extension only +// incremented in a later LO version). +// +// For another, UserInstallations have been seen in the wild where no extensions +// were installed per-user (any longer), but user/uno_packages/cache/registry/ +// com.sun.star.comp.deployment.component.PackageRegistryBackend/*.rdb files +// contained data nevertheless. +// +// When a LO upgrade is detected (i.e., no user/extensions/buildid or one +// containing an old build ID), then user/extensions and +// user/uno_packages/cache/registry/ +// com.sun.star.comp.deployment.component.PackageRegistryBackend/unorc are +// removed. That should prevent any problems starting the service manager due +// to old junk. Later on in Desktop::SynchronizeExtensionRepositories, the +// removed cache data is recreated. +// +// Multiple instances of soffice.bin can execute this code in parallel for a +// single UserInstallation, as it is called before RequestHandler is set up. +// Therefore, any errors here only lead to SAL_WARNs. +// +// At least in theory, this function could be removed again once no +// UserInstallation can be poisoned by old junk any more. +bool cleanExtensionCache() { + OUString buildId( + "${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("version") ":buildid}"); + rtl::Bootstrap::expandMacros(buildId); //TODO: detect failure + OUString extDir( + "${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") + ":UserInstallation}/user/extensions"); + rtl::Bootstrap::expandMacros(extDir); //TODO: detect failure + OUString buildIdFile(extDir + "/buildid"); + osl::File fr(buildIdFile); + osl::FileBase::RC rc = fr.open(osl_File_OpenFlag_Read); + switch (rc) { + case osl::FileBase::E_None: + { + rtl::ByteSequence s1; + rc = fr.readLine(s1); + osl::FileBase::RC rc2 = fr.close(); + SAL_WARN_IF( + rc2 != osl::FileBase::E_None, "desktop.app", + "cannot close " << fr.getURL() << " after reading: " << +rc2); + // readLine returns E_AGAIN for a zero-size file: + if (rc != osl::FileBase::E_None && rc != osl::FileBase::E_AGAIN) { + SAL_WARN( "desktop.app", "cannot read from " << fr.getURL() << ": " << +rc); + break; + } + OUString s2( + reinterpret_cast< char const * >(s1.getConstArray()), + s1.getLength(), RTL_TEXTENCODING_ISO_8859_1); + // using ISO 8859-1 avoids any and all conversion errors; the + // content should only be a subset of ASCII, anyway + if (s2 == buildId) { + return false; + } + break; + } + case osl::FileBase::E_NOENT: + break; + default: + SAL_WARN( "desktop.app", "cannot open " << fr.getURL() << " for reading: " << +rc); + break; + } + utl::removeTree(extDir); + OUString userRcFile( + "$UNO_USER_PACKAGES_CACHE/registry/" + "com.sun.star.comp.deployment.component.PackageRegistryBackend/unorc"); + rtl::Bootstrap::expandMacros(userRcFile); //TODO: detect failure + rc = osl::File::remove(userRcFile); + SAL_WARN_IF( + rc != osl::FileBase::E_None && rc != osl::FileBase::E_NOENT, "desktop.app", + "cannot remove file " << userRcFile << ": " << +rc); + rc = osl::Directory::createPath(extDir); + SAL_WARN_IF( + rc != osl::FileBase::E_None && rc != osl::FileBase::E_EXIST, "desktop.app", + "cannot create path " << extDir << ": " << +rc); + osl::File fw(buildIdFile); + rc = fw.open(osl_File_OpenFlag_Write | osl_File_OpenFlag_Create); + if (rc != osl::FileBase::E_None) { + SAL_WARN( "desktop.app", "cannot open " << fw.getURL() << " for writing: " << +rc); + return true; + } + OString buf(OUStringToOString(buildId, RTL_TEXTENCODING_UTF8)); + // using UTF-8 avoids almost all conversion errors (and buildid + // containing single surrogate halves should never happen, anyway); the + // content should only be a subset of ASCII, anyway + sal_uInt64 n = 0; + rc = fw.write(buf.getStr(), buf.getLength(), n); + SAL_WARN_IF( + (rc != osl::FileBase::E_None + || n != static_cast< sal_uInt32 >(buf.getLength())), + "desktop.app", + "cannot write to " << fw.getURL() << ": " << +rc << ", " << n); + rc = fw.close(); + SAL_WARN_IF( + rc != osl::FileBase::E_None, "desktop.app", + "cannot close " << fw.getURL() << " after writing: " << +rc); + return true; +} + +#endif + +bool shouldLaunchQuickstart() +{ + bool bQuickstart = Desktop::GetCommandLineArgs().IsQuickstart(); + if (!bQuickstart) + { + SfxItemSetFixed<SID_ATTR_QUICKLAUNCHER, SID_ATTR_QUICKLAUNCHER> aQLSet(SfxGetpApp()->GetPool()); + SfxApplication::GetOptions(aQLSet); + const SfxBoolItem* pLauncherItem = aQLSet.GetItemIfSet(SID_ATTR_QUICKLAUNCHER, false); + if (pLauncherItem) + bQuickstart = pLauncherItem->GetValue(); + } + return bQuickstart; +} + +void SetRestartState() { + try { + std::shared_ptr< comphelper::ConfigurationChanges > batch( + comphelper::ConfigurationChanges::create()); + officecfg::Setup::Office::OfficeRestartInProgress::set(true, batch); + batch->commit(); + } catch (css::uno::Exception) { + TOOLS_WARN_EXCEPTION("desktop.app", "ignoring"); + } +} + +void DoRestartActionsIfNecessary(bool quickstart) { + if (!quickstart) + return; + + try { + if (officecfg::Setup::Office::OfficeRestartInProgress::get()) { + std::shared_ptr< comphelper::ConfigurationChanges > batch( + comphelper::ConfigurationChanges::create()); + officecfg::Setup::Office::OfficeRestartInProgress::set( + false, batch); + batch->commit(); + css::office::Quickstart::createStart( + comphelper::getProcessComponentContext(), + shouldLaunchQuickstart()); + } + } catch (css::uno::Exception &) { + TOOLS_WARN_EXCEPTION("desktop.app", "ignoring"); + } +} + +void RemoveIconCacheDirectory() +{ + // See getIconCacheUrl in vcl/source/image/ImplImageTree.cxx + OUString sUrl = "${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER + "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/cache"; + rtl::Bootstrap::expandMacros(sUrl); + utl::UCBContentHelper::Kill(sUrl); +} + +} + +namespace { + +void runGraphicsRenderTests() +{ +#if !ENABLE_WASM_STRIP_PINGUSER + if (!utl::isProductVersionUpgraded(false)) + { + return; + } +#endif + GraphicsRenderTests TestObject; + TestObject.run(); +} + + +OUString MakeStartupErrorMessage(std::u16string_view aErrorMessage) +{ + return DpResId(STR_BOOTSTRAP_ERR_CANNOT_START) + "\n" + aErrorMessage; +} + + +// shows a simple error box with the given message ... but exits from these process ! +// Fatal errors can't be solved by the process ... nor any recovery can help. +// Mostly the installation was damaged and must be repaired manually .. or by calling +// setup again. +// On the other side we must make sure that no further actions will be possible within +// the current office process ! No pipe requests, no menu/toolbar/shortcut actions +// are allowed. Otherwise we will force a "crash inside a crash". +// That's why we have to use a special native message box here which does not use yield :-) + +void FatalError(const OUString& sMessage) +{ + OUString sProductKey = ::utl::Bootstrap::getProductKey(); + if ( sProductKey.isEmpty()) + { + osl_getExecutableFile( &sProductKey.pData ); + + ::sal_uInt32 nLastIndex = sProductKey.lastIndexOf('/'); + if ( nLastIndex > 0 ) + sProductKey = sProductKey.copy( nLastIndex+1 ); + } + + OUString sTitle = sProductKey + " - Fatal Error"; + Application::ShowNativeErrorBox (sTitle, sMessage); + std::cerr << sTitle << ": " << sMessage << std::endl; + _exit(EXITHELPER_FATAL_ERROR); +} + +} + +CommandLineArgs& Desktop::GetCommandLineArgs() +{ + static CommandLineArgs theCommandLineArgs; + return theCommandLineArgs; +} + +OUString ReplaceStringHookProc( const OUString& rStr ) +{ + const static OUString sBuildId(utl::Bootstrap::getBuildIdData("development")), + sBrandName(utl::ConfigManager::getProductName()), + sVersion(utl::ConfigManager::getProductVersion()), + sAboutBoxVersion(utl::ConfigManager::getAboutBoxProductVersion()), + sAboutBoxVersionSuffix(utl::ConfigManager::getAboutBoxProductVersionSuffix()), + sExtension(utl::ConfigManager::getProductExtension()); + + OUString sRet(rStr); + if (sRet.indexOf("%PRODUCT") != -1 || sRet.indexOf("%ABOUTBOX") != -1) + { + sRet = sRet.replaceAll( "%PRODUCTNAME", sBrandName ); + sRet = sRet.replaceAll( "%PRODUCTVERSION", sVersion ); + sRet = sRet.replaceAll( "%BUILDID", sBuildId ); + sRet = sRet.replaceAll( "%ABOUTBOXPRODUCTVERSIONSUFFIX", sAboutBoxVersionSuffix ); + sRet = sRet.replaceAll( "%ABOUTBOXPRODUCTVERSION", sAboutBoxVersion ); + sRet = sRet.replaceAll( "%PRODUCTEXTENSION", sExtension ); + } + + if ( sRet.indexOf( "%OOOVENDOR" ) != -1 ) + { + const static OUString sOOOVendor = utl::ConfigManager::getVendor(); + sRet = sRet.replaceAll( "%OOOVENDOR", sOOOVendor ); + } + + return sRet; +} + +Desktop::Desktop() + : m_bCleanedExtensionCache(false) + , m_bServicesRegistered(false) + , m_aBootstrapError(BE_OK) + , m_aBootstrapStatus(BS_OK) + , m_firstRunTimer( "desktop::Desktop m_firstRunTimer" ) +{ + m_firstRunTimer.SetTimeout(3000); // 3 sec. + m_firstRunTimer.SetInvokeHandler(LINK(this, Desktop, AsyncInitFirstRun)); +} + +Desktop::~Desktop() +{ +} + +void Desktop::Init() +{ + SetBootstrapStatus(BS_OK); + +#if HAVE_FEATURE_EXTENSIONS + m_bCleanedExtensionCache = cleanExtensionCache(); +#endif + + // We need to have service factory before going further, but see fdo#37195. + // Doing this will mmap common.rdb, making it not overwritable on windows, + // so this can't happen before the synchronization above. Lets rework this + // so that the above is called *from* CreateApplicationServiceManager or + // something to enforce this gotcha + try + { + InitApplicationServiceManager(); + } + catch (css::uno::Exception & e) + { + HandleBootstrapErrors( BE_UNO_SERVICEMANAGER, e.Message ); + std::abort(); + } + + // Check whether safe mode is enabled + const CommandLineArgs& rCmdLineArgs = GetCommandLineArgs(); + // Check if we are restarting from safe mode - in that case we don't want to enter it again + if (sfx2::SafeMode::hasRestartFlag()) + sfx2::SafeMode::removeRestartFlag(); + else if (rCmdLineArgs.IsSafeMode() || sfx2::SafeMode::hasFlag()) + Application::EnableSafeMode(); + + // When we are in SafeMode we need to do changes before the configuration + // gets read (langselect::prepareLocale() by UNO API -> Components::Components) + // This may prepare SafeMode or restore from it by moving data in + // the UserConfiguration directory + comphelper::BackupFileHelper::reactOnSafeMode(Application::IsSafeModeEnabled()); + + if ( m_aBootstrapError == BE_OK ) + { + try + { + if (!langselect::prepareLocale()) + { + SetBootstrapError( BE_LANGUAGE_MISSING, OUString() ); + } + } + catch (css::uno::Exception & e) + { + SetBootstrapError( BE_OFFICECONFIG_BROKEN, e.Message ); + } + + // test code for ProfileSafeMode to allow testing the fail + // of loading the office configuration initially. To use, + // either set to true and compile, or set a breakpoint + // in debugger and change the local bool + static bool bTryHardOfficeconfigBroken(false); // loplugin:constvars:ignore + + if (bTryHardOfficeconfigBroken) + { + SetBootstrapError(BE_OFFICECONFIG_BROKEN, OUString()); + } + } + + // start ipc thread only for non-remote offices + RequestHandler::Status aStatus = RequestHandler::Enable(true); + if ( aStatus == RequestHandler::IPC_STATUS_PIPE_ERROR ) + { +#if defined(ANDROID) || defined(EMSCRIPTEN) + // Ignore crack pipe errors on Android +#else + // Keep using this oddly named BE_PATHINFO_MISSING value + // for pipe-related errors on other platforms. Of course + // this crack with two (if not more) levels of our own + // error codes hiding the actual system error code is + // broken, but that is done all over the code, let's leave + // reengineering that to another year. + SetBootstrapError( BE_PATHINFO_MISSING, OUString() ); +#endif + } + else if ( aStatus == RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR ) + { + SetBootstrapError( BE_PATHINFO_MISSING, OUString() ); + } + else if ( aStatus == RequestHandler::IPC_STATUS_2ND_OFFICE ) + { + // 2nd office startup should terminate after sending cmdlineargs through pipe + SetBootstrapStatus(BS_TERMINATE); + } + else if ( !rCmdLineArgs.GetUnknown().isEmpty() + || rCmdLineArgs.IsHelp() || rCmdLineArgs.IsVersion() ) + { + // disable IPC thread in an instance that is just showing a help message + RequestHandler::Disable(); + } + pSignalHandler = osl_addSignalHandler(SalMainPipeExchangeSignal_impl, nullptr); +} + +void Desktop::InitFinished() +{ + CloseSplashScreen(); +} + +void Desktop::DeInit() +{ + try { + // instead of removing of the configManager just let it commit all the changes + utl::ConfigManager::storeConfigItems(); + FlushConfiguration(); + + // close splashscreen if it's still open + CloseSplashScreen(); + Reference< XComponent >( + comphelper::getProcessComponentContext(), UNO_QUERY_THROW )-> + dispose(); + // nobody should get a destroyed service factory... + ::comphelper::setProcessServiceFactory( nullptr ); + + // clear lockfile + m_xLockfile.reset(); + + RequestHandler::Disable(); + if( pSignalHandler ) + osl_removeSignalHandler( pSignalHandler ); + } catch (const RuntimeException&) { + // someone threw an exception during shutdown + // this will leave some garbage behind... + TOOLS_WARN_EXCEPTION("desktop.app", "exception throwing during shutdown, will leave some garbage behind"); + } +} + +bool Desktop::QueryExit() +{ + try + { + utl::ConfigManager::storeConfigItems(); + } + catch ( const RuntimeException& ) + { + } + + static constexpr OUStringLiteral SUSPEND_QUICKSTARTVETO = u"SuspendQuickstartVeto"; + + Reference< XDesktop2 > xDesktop = css::frame::Desktop::create( ::comphelper::getProcessComponentContext() ); + Reference< XPropertySet > xPropertySet(xDesktop, UNO_QUERY_THROW); + xPropertySet->setPropertyValue( SUSPEND_QUICKSTARTVETO, Any(true) ); + + bool bExit = xDesktop->terminate(); + + if ( !bExit ) + { + xPropertySet->setPropertyValue( SUSPEND_QUICKSTARTVETO, Any(false) ); + } + else if (!Application::IsEventTestingModeEnabled()) + { + FlushConfiguration(); + try + { + // it is no problem to call RequestHandler::Disable() more than once + // it also looks to be threadsafe + RequestHandler::Disable(); + } + catch ( const RuntimeException& ) + { + } + + m_xLockfile.reset(); + + } + + return bExit; +} + +void Desktop::Shutdown() +{ + framework::getDesktop(::comphelper::getProcessComponentContext())->shutdown(); +} + +void Desktop::HandleBootstrapPathErrors( ::utl::Bootstrap::Status aBootstrapStatus, std::u16string_view aDiagnosticMessage ) +{ + if ( aBootstrapStatus == ::utl::Bootstrap::DATA_OK ) + return; + + OUString aProductKey; + OUString aTemp; + + osl_getExecutableFile( &aProductKey.pData ); + sal_uInt32 lastIndex = aProductKey.lastIndexOf('/'); + if ( lastIndex > 0 ) + aProductKey = aProductKey.copy( lastIndex+1 ); + + aTemp = ::utl::Bootstrap::getProductKey( aProductKey ); + if ( !aTemp.isEmpty() ) + aProductKey = aTemp; + + OUString const aMessage(OUString::Concat(aDiagnosticMessage) + "\n"); + + std::unique_ptr<weld::MessageDialog> xBootstrapFailedBox(Application::CreateMessageDialog(nullptr, + VclMessageType::Warning, VclButtonsType::Ok, aMessage)); + xBootstrapFailedBox->set_title(aProductKey); + xBootstrapFailedBox->run(); +} + +// Create an error message depending on bootstrap failure code and an optional file url +OUString Desktop::CreateErrorMsgString( + utl::Bootstrap::FailureCode nFailureCode, + const OUString& aFileURL ) +{ + OUString aMsg; + bool bFileInfo = true; + + switch ( nFailureCode ) + { + /// the shared installation directory could not be located + case ::utl::Bootstrap::MISSING_INSTALL_DIRECTORY: + { + aMsg = DpResId(STR_BOOTSTRAP_ERR_PATH_INVALID); + bFileInfo = false; + } + break; + + /// the bootstrap INI file could not be found or read + case ::utl::Bootstrap::MISSING_BOOTSTRAP_FILE: + { + aMsg = DpResId(STR_BOOTSTRAP_ERR_FILE_MISSING); + } + break; + + /// the bootstrap INI is missing a required entry + /// the bootstrap INI contains invalid data + case ::utl::Bootstrap::MISSING_BOOTSTRAP_FILE_ENTRY: + case ::utl::Bootstrap::INVALID_BOOTSTRAP_FILE_ENTRY: + { + aMsg = DpResId(STR_BOOTSTRAP_ERR_FILE_CORRUPT); + } + break; + + /// the version locator INI file could not be found or read + case ::utl::Bootstrap::MISSING_VERSION_FILE: + { + aMsg = DpResId(STR_BOOTSTRAP_ERR_FILE_MISSING); + } + break; + + /// the version locator INI has no entry for this version + case ::utl::Bootstrap::MISSING_VERSION_FILE_ENTRY: + { + aMsg = DpResId(STR_BOOTSTRAP_ERR_NO_SUPPORT); + } + break; + + /// the user installation directory does not exist + case ::utl::Bootstrap::MISSING_USER_DIRECTORY: + { + aMsg = DpResId(STR_BOOTSTRAP_ERR_DIR_MISSING); + } + break; + + /// some bootstrap data was invalid in unexpected ways + case ::utl::Bootstrap::INVALID_BOOTSTRAP_DATA: + { + aMsg = DpResId(STR_BOOTSTRAP_ERR_INTERNAL); + bFileInfo = false; + } + break; + + case ::utl::Bootstrap::INVALID_VERSION_FILE_ENTRY: + { + // This needs to be improved, see #i67575#: + aMsg = "Invalid version file entry"; + bFileInfo = false; + } + break; + + case ::utl::Bootstrap::NO_FAILURE: + { + OSL_ASSERT(false); + } + break; + } + + if ( bFileInfo ) + { + OUString aMsgString( aMsg ); + OUString aFilePath; + + osl::File::getSystemPathFromFileURL( aFileURL, aFilePath ); + + aMsgString = aMsgString.replaceFirst( "$1", aFilePath ); + aMsg = aMsgString; + } + + return MakeStartupErrorMessage( aMsg ); +} + +void Desktop::HandleBootstrapErrors( + BootstrapError aBootstrapError, OUString const & aErrorMessage ) +{ + if ( aBootstrapError == BE_PATHINFO_MISSING ) + { + OUString aErrorMsg; + OUString aBuffer; + utl::Bootstrap::Status aBootstrapStatus; + utl::Bootstrap::FailureCode nFailureCode; + + aBootstrapStatus = ::utl::Bootstrap::checkBootstrapStatus( aBuffer, nFailureCode ); + if ( aBootstrapStatus != ::utl::Bootstrap::DATA_OK ) + { + switch ( nFailureCode ) + { + case ::utl::Bootstrap::MISSING_INSTALL_DIRECTORY: + case ::utl::Bootstrap::INVALID_BOOTSTRAP_DATA: + { + aErrorMsg = CreateErrorMsgString( nFailureCode, OUString() ); + } + break; + + /// the bootstrap INI file could not be found or read + /// the bootstrap INI is missing a required entry + /// the bootstrap INI contains invalid data + case ::utl::Bootstrap::MISSING_BOOTSTRAP_FILE_ENTRY: + case ::utl::Bootstrap::INVALID_BOOTSTRAP_FILE_ENTRY: + case ::utl::Bootstrap::MISSING_BOOTSTRAP_FILE: + { + OUString aBootstrapFileURL; + + utl::Bootstrap::locateBootstrapFile( aBootstrapFileURL ); + aErrorMsg = CreateErrorMsgString( nFailureCode, aBootstrapFileURL ); + } + break; + + /// the version locator INI file could not be found or read + /// the version locator INI has no entry for this version + /// the version locator INI entry is not a valid directory URL + case ::utl::Bootstrap::INVALID_VERSION_FILE_ENTRY: + case ::utl::Bootstrap::MISSING_VERSION_FILE_ENTRY: + case ::utl::Bootstrap::MISSING_VERSION_FILE: + { + OUString aVersionFileURL; + + utl::Bootstrap::locateVersionFile( aVersionFileURL ); + aErrorMsg = CreateErrorMsgString( nFailureCode, aVersionFileURL ); + } + break; + + /// the user installation directory does not exist + case ::utl::Bootstrap::MISSING_USER_DIRECTORY: + { + OUString aUserInstallationURL; + + utl::Bootstrap::locateUserInstallation( aUserInstallationURL ); + aErrorMsg = CreateErrorMsgString( nFailureCode, aUserInstallationURL ); + } + break; + + case ::utl::Bootstrap::NO_FAILURE: + { + OSL_ASSERT(false); + } + break; + } + + HandleBootstrapPathErrors( aBootstrapStatus, aErrorMsg ); + } + } + else if ( aBootstrapError == BE_UNO_SERVICEMANAGER || aBootstrapError == BE_UNO_SERVICE_CONFIG_MISSING ) + { + // UNO service manager is not available. VCL needs a UNO service manager to display a message box!!! + // Currently we are not able to display a message box with a service manager due to this limitations inside VCL. + + // When UNO is not properly initialized, all kinds of things can fail + // and cause the process to crash. To give the user a hint even if + // generating and displaying a message box below crashes, print a + // hard-coded message on stderr first: + std::cerr + << "The application cannot be started.\n" + // STR_BOOTSTRAP_ERR_CANNOT_START + << (aBootstrapError == BE_UNO_SERVICEMANAGER + ? "The component manager is not available.\n" + // STR_BOOTSTRAP_ERR_NO_SERVICE + : "The configuration service is not available.\n"); + // STR_BOOTSTRAP_ERR_NO_CFG_SERVICE + if ( !aErrorMessage.isEmpty() ) + { + std::cerr << "(\"" << aErrorMessage << "\")\n"; + } + + // First sentence. We cannot bootstrap office further! + OUString aDiagnosticMessage = DpResId(STR_BOOTSTRAP_ERR_NO_CFG_SERVICE) + "\n"; + if ( !aErrorMessage.isEmpty() ) + { + aDiagnosticMessage += "(\"" + aErrorMessage + "\")\n"; + } + + // Due to the fact the we haven't a backup applicat.rdb file anymore it is not possible to + // repair the installation with the setup executable besides the office executable. Now + // we have to ask the user to start the setup on CD/installation directory manually!! + aDiagnosticMessage += DpResId(STR_ASK_START_SETUP_MANUALLY); + + FatalError(MakeStartupErrorMessage(aDiagnosticMessage)); + } + else if ( aBootstrapError == BE_OFFICECONFIG_BROKEN ) + { + // set flag at BackupFileHelper to be able to know if _exit was called and + // actions are executed after this. This method we are in will not return, + // but end up in a _exit() call + comphelper::BackupFileHelper::setExitWasCalled(); + + // enter safe mode, too + sfx2::SafeMode::putFlag(); + + OUString msg(DpResId(STR_CONFIG_ERR_ACCESS_GENERAL)); + if (!aErrorMessage.isEmpty()) { + msg += "\n(\"" + aErrorMessage + "\")"; + } + FatalError(MakeStartupErrorMessage(msg)); + } + else if ( aBootstrapError == BE_USERINSTALL_FAILED ) + { + OUString aDiagnosticMessage = DpResId(STR_BOOTSTRAP_ERR_USERINSTALL_FAILED); + FatalError(MakeStartupErrorMessage(aDiagnosticMessage)); + } + else if ( aBootstrapError == BE_LANGUAGE_MISSING ) + { + OUString aDiagnosticMessage = DpResId(STR_BOOTSTRAP_ERR_LANGUAGE_MISSING); + FatalError(MakeStartupErrorMessage(aDiagnosticMessage)); + } + else if (( aBootstrapError == BE_USERINSTALL_NOTENOUGHDISKSPACE ) || + ( aBootstrapError == BE_USERINSTALL_NOWRITEACCESS )) + { + OUString aUserInstallationURL; + OUString aUserInstallationPath; + utl::Bootstrap::locateUserInstallation( aUserInstallationURL ); + osl::File::getSystemPathFromFileURL( aUserInstallationURL, aUserInstallationPath ); + + OUString aDiagnosticMessage; + if ( aBootstrapError == BE_USERINSTALL_NOTENOUGHDISKSPACE ) + aDiagnosticMessage = DpResId(STR_BOOTSTRAP_ERR_NOTENOUGHDISKSPACE); + else + aDiagnosticMessage = DpResId(STR_BOOTSTRAP_ERR_NOACCESSRIGHTS); + aDiagnosticMessage += aUserInstallationPath; + + FatalError(MakeStartupErrorMessage(aDiagnosticMessage)); + } +} + + +namespace { + + +#if HAVE_FEATURE_BREAKPAD +void handleCrashReport() +{ + static constexpr OUStringLiteral SERVICENAME_CRASHREPORT = u"com.sun.star.comp.svx.CrashReportUI"; + + css::uno::Reference< css::uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + + Reference< css::frame::XSynchronousDispatch > xRecoveryUI( + xContext->getServiceManager()->createInstanceWithContext(SERVICENAME_CRASHREPORT, xContext), + css::uno::UNO_QUERY_THROW); + + Reference< css::util::XURLTransformer > xURLParser = + css::util::URLTransformer::create(::comphelper::getProcessComponentContext()); + + css::util::URL aURL; + css::uno::Any aRet = xRecoveryUI->dispatchWithReturnValue(aURL, css::uno::Sequence< css::beans::PropertyValue >()); + bool bRet = false; + aRet >>= bRet; +} +#endif + +#if !defined ANDROID +void handleSafeMode() +{ + css::uno::Reference< css::uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + + Reference< css::frame::XSynchronousDispatch > xSafeModeUI( + xContext->getServiceManager()->createInstanceWithContext("com.sun.star.comp.svx.SafeModeUI", xContext), + css::uno::UNO_QUERY_THROW); + + css::util::URL aURL; + css::uno::Any aRet = xSafeModeUI->dispatchWithReturnValue(aURL, css::uno::Sequence< css::beans::PropertyValue >()); + bool bRet = false; + aRet >>= bRet; +} +#endif + +/** @short check if recovery must be started or not. + + @param bCrashed [boolean ... out!] + the office crashed last times. + But may be there are no recovery data. + Useful to trigger the error report tool without + showing the recovery UI. + + @param bRecoveryDataExists [boolean ... out!] + there exists some recovery data. + + @param bSessionDataExists [boolean ... out!] + there exists some session data. + Because the user may be logged out last time from its + unix session... +*/ +void impl_checkRecoveryState(bool& bCrashed , + bool& bRecoveryDataExists, + bool& bSessionDataExists ) +{ + bCrashed = officecfg::Office::Recovery::RecoveryInfo::Crashed::get() +#if HAVE_FEATURE_BREAKPAD + || CrashReporter::crashReportInfoExists(); +#else + ; +#endif + bool elements = officecfg::Office::Recovery::RecoveryList::get()-> + hasElements(); + bool session + = officecfg::Office::Recovery::RecoveryInfo::SessionData::get(); + bRecoveryDataExists = elements && !session; + bSessionDataExists = elements && session; +} + +Reference< css::frame::XSynchronousDispatch > g_xRecoveryUI; + +template <class Ref> +struct RefClearGuard +{ + Ref& m_Ref; + RefClearGuard(Ref& ref) : m_Ref(ref) {} + ~RefClearGuard() { m_Ref.clear(); } +}; + +/* @short start the recovery wizard. + + @param bEmergencySave + differs between EMERGENCY_SAVE and RECOVERY +*/ +#if !ENABLE_WASM_STRIP_RECOVERYUI +bool impl_callRecoveryUI(bool bEmergencySave , + bool bExistsRecoveryData) +{ + constexpr OUStringLiteral COMMAND_EMERGENCYSAVE = u"vnd.sun.star.autorecovery:/doEmergencySave"; + constexpr OUStringLiteral COMMAND_RECOVERY = u"vnd.sun.star.autorecovery:/doAutoRecovery"; + + css::uno::Reference< css::uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + + g_xRecoveryUI.set( + xContext->getServiceManager()->createInstanceWithContext("com.sun.star.comp.svx.RecoveryUI", xContext), + css::uno::UNO_QUERY_THROW); + RefClearGuard<Reference< css::frame::XSynchronousDispatch >> refClearGuard(g_xRecoveryUI); + + Reference< css::util::XURLTransformer > xURLParser = + css::util::URLTransformer::create(xContext); + + css::util::URL aURL; + if (bEmergencySave) + aURL.Complete = COMMAND_EMERGENCYSAVE; + else if (bExistsRecoveryData) + aURL.Complete = COMMAND_RECOVERY; + else + return false; + + xURLParser->parseStrict(aURL); + + css::uno::Any aRet = g_xRecoveryUI->dispatchWithReturnValue(aURL, css::uno::Sequence< css::beans::PropertyValue >()); + bool bRet = false; + aRet >>= bRet; + return bRet; +} +#endif + +bool impl_bringToFrontRecoveryUI() +{ + Reference< css::frame::XSynchronousDispatch > xRecoveryUI(g_xRecoveryUI); + if (!xRecoveryUI.is()) + return false; + + css::util::URL aURL; + aURL.Complete = "vnd.sun.star.autorecovery:/doBringToFront"; + Reference< css::util::XURLTransformer > xURLParser = + css::util::URLTransformer::create(::comphelper::getProcessComponentContext()); + xURLParser->parseStrict(aURL); + + css::uno::Any aRet = xRecoveryUI->dispatchWithReturnValue(aURL, css::uno::Sequence< css::beans::PropertyValue >()); + bool bRet = false; + aRet >>= bRet; + return bRet; +} + +} + +namespace { + +void restartOnMac(bool passArguments) { +#if defined MACOSX + RequestHandler::Disable(); +#if HAVE_FEATURE_MACOSX_SANDBOX + (void) passArguments; // avoid warnings + OUString aMessage = DpResId(STR_LO_MUST_BE_RESTARTED); + + std::unique_ptr<weld::MessageDialog> xRestartBox(Application::CreateMessageDialog(nullptr, + VclMessageType::Warning, VclButtonsType::Ok, aMessage)); + xRestartBox->run(); +#else + OUString execUrl; + OSL_VERIFY(osl_getExecutableFile(&execUrl.pData) == osl_Process_E_None); + OUString execPath; + OString execPath8; + if ((osl::FileBase::getSystemPathFromFileURL(execUrl, execPath) + != osl::FileBase::E_None) || + !execPath.convertToString( + &execPath8, osl_getThreadTextEncoding(), + (RTL_UNICODETOTEXT_FLAGS_UNDEFINED_ERROR | + RTL_UNICODETOTEXT_FLAGS_INVALID_ERROR))) + { + std::abort(); + } + std::vector< OString > args { execPath8 }; + bool wait = false; + if (passArguments) { + sal_uInt32 n = osl_getCommandArgCount(); + for (sal_uInt32 i = 0; i < n; ++i) { + OUString arg; + osl_getCommandArg(i, &arg.pData); + if (arg.match("--accept=")) { + wait = true; + } + OString arg8; + if (!arg.convertToString( + &arg8, osl_getThreadTextEncoding(), + (RTL_UNICODETOTEXT_FLAGS_UNDEFINED_ERROR | + RTL_UNICODETOTEXT_FLAGS_INVALID_ERROR))) + { + std::abort(); + } + args.push_back(arg8); + } + } + std::vector< char const * > argPtrs; + for (auto const& elem : args) + { + argPtrs.push_back(elem.getStr()); + } + argPtrs.push_back(nullptr); + execv(execPath8.getStr(), const_cast< char ** >(argPtrs.data())); + if (errno == ENOTSUP) { // happens when multithreaded on macOS < 10.6 + pid_t pid = fork(); + if (pid == 0) { + execv(execPath8.getStr(), const_cast< char ** >(argPtrs.data())); + } else if (pid > 0) { + // Two simultaneously running soffice processes lead to two dock + // icons, so avoid waiting here unless it must be assumed that the + // process invoking soffice itself wants to wait for soffice to + // finish: + if (!wait) { + return; + } + int stat; + if (waitpid(pid, &stat, 0) == pid && WIFEXITED(stat)) { + _exit(WEXITSTATUS(stat)); + } + } + } + std::abort(); +#endif +#else + (void) passArguments; // avoid warnings +#endif +} + +#if HAVE_FEATURE_UPDATE_MAR +bool isTimeForUpdateCheck() +{ + sal_uInt64 nLastUpdate = officecfg::Office::Update::Update::LastUpdateTime::get(); + sal_uInt64 nNow = tools::Time::GetSystemTicks(); + + sal_uInt64 n7DayInMS = 1000 * 60 * 60 * 12 * 1; // 12 hours in ms + if (nNow - n7DayInMS >= nLastUpdate) + return true; + + return false; +} +#endif + +} + +void Desktop::Exception(ExceptionCategory nCategory) +{ + // protect against recursive calls + static bool bInException = false; + +#if HAVE_FEATURE_BREAKPAD + CrashReporter::removeExceptionHandler(); // disallow re-entry +#endif + + SystemWindowFlags nOldMode = Application::GetSystemWindowMode(); + Application::SetSystemWindowMode( nOldMode & ~SystemWindowFlags::NOAUTOMODE ); + if ( bInException ) + { + Application::Abort( OUString() ); + } + + bInException = true; + const CommandLineArgs& rArgs = GetCommandLineArgs(); + + // save all modified documents ... if it's allowed doing so. + bool bRestart = false; + bool bAllowRecoveryAndSessionManagement = ( + ( !rArgs.IsNoRestore() ) && // some use cases of office must work without recovery + ( !rArgs.IsHeadless() ) && + ( nCategory != ExceptionCategory::UserInterface ) && // recovery can't work without UI ... but UI layer seems to be the reason for this crash + ( Application::IsInExecute() ) // crashes during startup and shutdown should be ignored (they indicate a corrupted installation...) + ); + if ( bAllowRecoveryAndSessionManagement ) + { + // Save all open documents so they will be reopened + // the next time the application is started + // returns true if at least one document could be saved... +#if !ENABLE_WASM_STRIP_RECOVERYUI + bRestart = impl_callRecoveryUI( + true , // force emergency save + false); +#endif + } + + FlushConfiguration(); + + m_xLockfile.reset(); + + if( bRestart ) + { + RequestHandler::Disable(); + if( pSignalHandler ) + osl_removeSignalHandler( pSignalHandler ); + + restartOnMac(false); +#if !ENABLE_WASM_STRIP_SPLASH + if ( m_rSplashScreen.is() ) + m_rSplashScreen->reset(); +#endif + + _exit( EXITHELPER_CRASH_WITH_RESTART ); + } + else + { + Application::Abort( OUString() ); + } + + OSL_ASSERT(false); // unreachable +} + +void Desktop::AppEvent( const ApplicationEvent& rAppEvent ) +{ + HandleAppEvent( rAppEvent ); +} + +namespace { + +class JVMloadThread : public salhelper::Thread { +public: + JVMloadThread() : salhelper::Thread("Preload JVM thread") + { + } + +private: + virtual void execute() override final + { + Reference< XMultiServiceFactory > xSMgr = comphelper::getProcessServiceFactory(); + + Reference< css::loader::XImplementationLoader > xJavaComponentLoader( + xSMgr->createInstance("com.sun.star.comp.stoc.JavaComponentLoader"), + css::uno::UNO_QUERY_THROW); + + if (xJavaComponentLoader.is()) + { + const css::uno::Reference< ::com::sun::star::registry::XRegistryKey > xRegistryKey; + try + { + xJavaComponentLoader->activate("", "", "", xRegistryKey); + } + catch (...) + { + SAL_WARN("desktop.app", "Cannot activate factory during JVM preloading"); + } + } + } +}; + +struct ExecuteGlobals +{ + Reference < css::document::XDocumentEventListener > xGlobalBroadcaster; + bool bRestartRequested; + bool bUseSystemFileDialog; + std::unique_ptr<SvtCTLOptions> pCTLLanguageOptions; + std::unique_ptr<SvtPathOptions> pPathOptions; + rtl::Reference< JVMloadThread > xJVMloadThread; + + ExecuteGlobals() + : bRestartRequested( false ) + , bUseSystemFileDialog( true ) + {} +}; + +} + +static ExecuteGlobals* pExecGlobals = nullptr; + +int Desktop::Main() +{ + pExecGlobals = new ExecuteGlobals(); + + // Remember current context object + css::uno::ContextLayer layer( css::uno::getCurrentContext() ); + + if ( m_aBootstrapError != BE_OK ) + { + HandleBootstrapErrors( m_aBootstrapError, m_aBootstrapErrorMessage ); + return EXIT_FAILURE; + } + + BootstrapStatus eStatus = GetBootstrapStatus(); + if (eStatus == BS_TERMINATE) { + return EXIT_SUCCESS; + } + + // Detect desktop environment - need to do this as early as possible + css::uno::setCurrentContext( new DesktopContext( css::uno::getCurrentContext() ) ); + + if (officecfg::Office::Common::Misc::PreloadJVM::get() && pExecGlobals) + { + SAL_INFO("desktop.app", "Preload JVM"); + + // pre-load JVM + pExecGlobals->xJVMloadThread = new JVMloadThread(); + pExecGlobals->xJVMloadThread->launch(); + } + + CommandLineArgs& rCmdLineArgs = GetCommandLineArgs(); + + Translate::SetReadStringHook(ReplaceStringHookProc); + + // Startup screen +#if !ENABLE_WASM_STRIP_SPLASH + OpenSplashScreen(); +#endif + + SetSplashScreenProgress(10); + + userinstall::Status inst_fin = userinstall::finalize(); + if (inst_fin != userinstall::EXISTED && inst_fin != userinstall::CREATED) + { + SAL_WARN( "desktop.app", "userinstall failed: " << inst_fin); + if ( inst_fin == userinstall::ERROR_NO_SPACE ) + HandleBootstrapErrors( + BE_USERINSTALL_NOTENOUGHDISKSPACE, OUString() ); + else if ( inst_fin == userinstall::ERROR_CANT_WRITE ) + HandleBootstrapErrors( BE_USERINSTALL_NOWRITEACCESS, OUString() ); + else + HandleBootstrapErrors( BE_USERINSTALL_FAILED, OUString() ); + return EXIT_FAILURE; + } + // refresh path information + utl::Bootstrap::reloadData(); + SetSplashScreenProgress(20); + + Reference< XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + + Reference< XRestartManager > xRestartManager( OfficeRestartManager::get(xContext) ); + + Reference< XDesktop2 > xDesktop; + + RegisterServices(); + + SetSplashScreenProgress(25); + +#if HAVE_FEATURE_DESKTOP && !defined(EMSCRIPTEN) + // check user installation directory for lockfile so we can be sure + // there is no other instance using our data files from a remote host + + bool bMustLockProfile = ( getenv( "SAL_NOLOCK_PROFILE" ) == nullptr ); + if ( bMustLockProfile ) + { + m_xLockfile.reset(new Lockfile); + + if ( !rCmdLineArgs.IsHeadless() && !rCmdLineArgs.IsInvisible() && + !rCmdLineArgs.IsNoLockcheck() && !m_xLockfile->check( Lockfile_execWarning )) + { + // Lockfile exists, and user clicked 'no' + return EXIT_FAILURE; + } + } + + // check if accessibility is enabled but not working and allow to quit + if( Application::GetSettings().GetMiscSettings().GetEnableATToolSupport() ) + { + if( !InitAccessBridge() ) + return EXIT_FAILURE; + } +#endif + + // terminate if requested... + if( rCmdLineArgs.IsTerminateAfterInit() ) + return EXIT_SUCCESS; + + // Read the common configuration items for optimization purpose + if ( !InitializeConfiguration() ) + return EXIT_FAILURE; + +#if HAVE_FEATURE_UPDATE_MAR + const char* pUpdaterTestEnable = std::getenv("LIBO_UPDATER_TEST_ENABLE"); + if (pUpdaterTestEnable || officecfg::Office::Update::Update::Enabled::get()) + { + // check if we just updated + const char* pUpdaterRunning = std::getenv("LIBO_UPDATER_TEST_RUNNING"); + bool bUpdateRunning = officecfg::Office::Update::Update::UpdateRunning::get() || pUpdaterRunning; + if (bUpdateRunning) + { + OUString aSeeAlso = officecfg::Office::Update::Update::SeeAlso::get(); + OUString aOldBuildID = officecfg::Office::Update::Update::OldBuildID::get(); + + OUString aBuildID = Updater::getBuildID(); + if (aOldBuildID == aBuildID) + { + Updater::log("Old and new Build ID are the same. No Updating took place."); + } + else + { + if (!aSeeAlso.isEmpty()) + { + SAL_INFO("desktop.updater", "See also: " << aSeeAlso); + Reference< css::system::XSystemShellExecute > xSystemShell( + SystemShellExecute::create(::comphelper::getProcessComponentContext()) ); + + xSystemShell->execute( aSeeAlso, OUString(), SystemShellExecuteFlags::URIS_ONLY ); + } + } + + // reset all the configuration values, + // all values need to be read before this code + std::shared_ptr< comphelper::ConfigurationChanges > batch( + comphelper::ConfigurationChanges::create()); + officecfg::Office::Update::Update::UpdateRunning::set(false, batch); + officecfg::Office::Update::Update::SeeAlso::set(OUString(), batch); + officecfg::Office::Update::Update::OldBuildID::set(OUString(), batch); + batch->commit(); + + Updater::removeUpdateFiles(); + } + + osl::DirectoryItem aUpdateFile; + osl::DirectoryItem::get(Updater::getUpdateFileURL(), aUpdateFile); + + const char* pUpdaterTestUpdate = std::getenv("LIBO_UPDATER_TEST_UPDATE"); + const char* pForcedUpdateCheck = std::getenv("LIBO_UPDATER_TEST_UPDATE_CHECK"); + if (pUpdaterTestUpdate || aUpdateFile.is()) + { + OUString aBuildID("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("version") ":buildid}"); + rtl::Bootstrap::expandMacros(aBuildID); + std::shared_ptr< comphelper::ConfigurationChanges > batch( + comphelper::ConfigurationChanges::create()); + officecfg::Office::Update::Update::OldBuildID::set(aBuildID, batch); + officecfg::Office::Update::Update::UpdateRunning::set(true, batch); + batch->commit(); + + // make sure the change is written to the configuration before we start the update + css::uno::Reference<css::util::XFlushable> xFlushable(css::configuration::theDefaultProvider::get(xContext), UNO_QUERY); + xFlushable->flush(); + // avoid the old oosplash staying around + CloseSplashScreen(); + bool bSuccess = update(); + if (bSuccess) + return EXIT_SUCCESS; + } + else if (isTimeForUpdateCheck() || pForcedUpdateCheck) + { + sal_uInt64 nNow = tools::Time::GetSystemTicks(); + Updater::log("Update Check Time: " + OUString::number(nNow)); + std::shared_ptr< comphelper::ConfigurationChanges > batch( + comphelper::ConfigurationChanges::create()); + officecfg::Office::Update::Update::LastUpdateTime::set(nNow, batch); + batch->commit(); + m_aUpdateThread = std::thread(update_checker); + } + } +#endif + + SetSplashScreenProgress(30); + + // create title string + OUString aTitle(ReplaceStringHookProc(RID_APPTITLE)); +#ifdef DBG_UTIL + //include buildid in non product builds + aTitle += " [" + utl::Bootstrap::getBuildIdData("development") + "]"; +#endif + + SetDisplayName( aTitle ); + SetSplashScreenProgress(35); + pExecGlobals->pPathOptions.reset( new SvtPathOptions); + SetSplashScreenProgress(40); + + xDesktop = css::frame::Desktop::create( xContext ); + + // create service for loading SFX (still needed in startup) + pExecGlobals->xGlobalBroadcaster = Reference < css::document::XDocumentEventListener > + ( css::frame::theGlobalEventBroadcaster::get(xContext), UNO_SET_THROW ); + + /* ensure existence of a default window that messages can be dispatched to + This is for the benefit of testtool which uses PostUserEvent extensively + and else can deadlock while creating this window from another thread while + the main thread is not yet in the event loop. + */ + Application::GetDefaultDevice(); + +#if HAVE_FEATURE_EXTENSIONS + // Check if bundled or shared extensions were added /removed + // and process those extensions (has to be done before checking + // the extension dependencies! + SynchronizeExtensionRepositories(m_bCleanedExtensionCache, this); + bool bAbort = CheckExtensionDependencies(); + if ( bAbort ) + return EXIT_FAILURE; + + if (inst_fin == userinstall::CREATED) + { + Migration::migrateSettingsIfNecessary(); + } +#endif + + // keep a language options instance... + pExecGlobals->pCTLLanguageOptions.reset( new SvtCTLOptions(true)); + + css::document::DocumentEvent aEvent; + aEvent.EventName = "OnStartApp"; + pExecGlobals->xGlobalBroadcaster->documentEventOccured(aEvent); + + SetSplashScreenProgress(50); + + // Backing Component + bool bCrashed = false; + bool bExistsRecoveryData = false; + bool bExistsSessionData = false; + + impl_checkRecoveryState(bCrashed, bExistsRecoveryData, bExistsSessionData); + + OUString pidfileName = rCmdLineArgs.GetPidfileName(); + if ( !pidfileName.isEmpty() ) + { + OUString pidfileURL; + + if ( osl_getFileURLFromSystemPath(pidfileName.pData, &pidfileURL.pData) == osl_File_E_None ) + { + osl::File pidfile( pidfileURL ); + osl::FileBase::RC rc; + + osl::File::remove( pidfileURL ); + if ( (rc = pidfile.open( osl_File_OpenFlag_Write | osl_File_OpenFlag_Create ) ) == osl::File::E_None ) + { + OString pid( OString::number( GETPID() ) ); + sal_uInt64 written = 0; + if ( pidfile.write(pid.getStr(), pid.getLength(), written) != osl::File::E_None ) + { + SAL_WARN("desktop.app", "cannot write pidfile " << pidfile.getURL()); + } + pidfile.close(); + } + else + { + SAL_WARN("desktop.app", "cannot open pidfile " << pidfile.getURL() << rc); + } + } + else + { + SAL_WARN("desktop.app", "cannot get pidfile URL from path" << pidfileName); + } + } + + if ( rCmdLineArgs.IsHeadless() || rCmdLineArgs.IsEventTesting() ) + { + // Ensure that we use not the system file dialogs as + // headless mode relies on Application::EnableHeadlessMode() + // which does only work for VCL dialogs!! + pExecGlobals->bUseSystemFileDialog = officecfg::Office::Common::Misc::UseSystemFileDialog::get(); + std::shared_ptr< comphelper::ConfigurationChanges > xChanges( + comphelper::ConfigurationChanges::create()); + officecfg::Office::Common::Misc::UseSystemFileDialog::set( false, xChanges ); + xChanges->commit(); + } + + pExecGlobals->bRestartRequested = xRestartManager->isRestartRequested(true); + if ( !pExecGlobals->bRestartRequested ) + { + if ((!rCmdLineArgs.WantsToLoadDocument() && !rCmdLineArgs.IsInvisible() && !rCmdLineArgs.IsHeadless() && !rCmdLineArgs.IsQuickstart()) && + (SvtModuleOptions().IsModuleInstalled(SvtModuleOptions::EModule::STARTMODULE)) && + (!bExistsRecoveryData ) && + (!bExistsSessionData ) && + (!Application::AnyInput( VclInputFlags::APPEVENT ) )) + { + ShowBackingComponent(this); + } + } + + SetSplashScreenProgress(55); + + svtools::ApplyFontSubstitutionsToVcl(); + + SvtTabAppearanceCfg aAppearanceCfg; + SvtTabAppearanceCfg::SetInitialized(); + aAppearanceCfg.SetApplicationDefaults( this ); + SvtAccessibilityOptions aOptions; + aOptions.SetVCLSettings(); + SetSplashScreenProgress(60); + + if ( !pExecGlobals->bRestartRequested ) + { + Application::SetFilterHdl( LINK( this, Desktop, ImplInitFilterHdl ) ); + + // Preload function depends on an initialized sfx application! + SetSplashScreenProgress(75); + + // use system window dialogs + Application::SetSystemWindowMode( SystemWindowFlags::DIALOG ); + + SetSplashScreenProgress(80); + + if ( !rCmdLineArgs.IsInvisible() && + !rCmdLineArgs.IsNoQuickstart() ) + InitializeQuickstartMode( xContext ); + + if ( xDesktop.is() ) + xDesktop->addTerminateListener( new RequestHandlerController ); + SetSplashScreenProgress(100); + + // FIXME: move this somewhere sensible. +#if HAVE_FEATURE_OPENCL + CheckOpenCLCompute(xDesktop); +#endif + + //Running the VCL graphics rendering tests + runGraphicsRenderTests(); + + // Post user event to startup first application component window + // We have to send this OpenClients message short before execute() to + // minimize the risk that this message overtakes type detection construction!! + Application::PostUserEvent( LINK( this, Desktop, OpenClients_Impl ) ); + + // Post event to enable acceptors + Application::PostUserEvent( LINK( this, Desktop, EnableAcceptors_Impl) ); + + // call Application::Execute to process messages in vcl message loop +#if HAVE_FEATURE_JAVA + // The JavaContext contains an interaction handler which is used when + // the creation of a Java Virtual Machine fails + css::uno::ContextLayer layer2( + new svt::JavaContext( css::uno::getCurrentContext() ) ); +#endif + // check whether the shutdown is caused by restart just before entering the Execute + pExecGlobals->bRestartRequested = pExecGlobals->bRestartRequested || + xRestartManager->isRestartRequested(true); + + if ( !pExecGlobals->bRestartRequested ) + { + // if this run of the office is triggered by restart, some additional actions should be done + DoRestartActionsIfNecessary( !rCmdLineArgs.IsInvisible() && !rCmdLineArgs.IsNoQuickstart() ); + + Execute(); + } + } + else + { + if (xDesktop.is()) + xDesktop->terminate(); + } + // CAUTION: you do not necessarily get here e.g. on the Mac. + // please put all deinitialization code into doShutdown + return doShutdown(); +} + +int Desktop::doShutdown() +{ + if( ! pExecGlobals ) + return EXIT_SUCCESS; + + if (m_aUpdateThread.joinable()) + m_aUpdateThread.join(); + + if (pExecGlobals->xJVMloadThread.is()) + { + pExecGlobals->xJVMloadThread->join(); + pExecGlobals->xJVMloadThread.clear(); + } + + pExecGlobals->bRestartRequested = pExecGlobals->bRestartRequested || + OfficeRestartManager::get(comphelper::getProcessComponentContext())-> + isRestartRequested(true); + if ( pExecGlobals->bRestartRequested ) + SetRestartState(); + + // Restore old value + const CommandLineArgs& rCmdLineArgs = GetCommandLineArgs(); + if ( rCmdLineArgs.IsHeadless() || rCmdLineArgs.IsEventTesting() ) + { + std::shared_ptr< comphelper::ConfigurationChanges > xChanges( + comphelper::ConfigurationChanges::create()); + officecfg::Office::Common::Misc::UseSystemFileDialog::set( pExecGlobals->bUseSystemFileDialog, xChanges ); + xChanges->commit(); + } + + OUString pidfileName = rCmdLineArgs.GetPidfileName(); + if ( !pidfileName.isEmpty() ) + { + OUString pidfileURL; + + if ( osl_getFileURLFromSystemPath(pidfileName.pData, &pidfileURL.pData) == osl_File_E_None ) + { + if ( osl::File::remove( pidfileURL ) != osl::FileBase::E_None ) + { + SAL_WARN("desktop.app", "shutdown: cannot remove pidfile " << pidfileURL); + } + } + else + { + SAL_WARN("desktop.app", "shutdown: cannot get pidfile URL from path" << pidfileName); + } + } + + // remove temp directory + RemoveTemporaryDirectory(); + flatpak::removeTemporaryHtmlDirectory(); + + // flush evtl. configuration changes so that all config files in user + // dir are written + FlushConfiguration(); + + if (pExecGlobals->bRestartRequested) + { + // tdf#128523 + RemoveIconCacheDirectory(); + + // a restart is already requested, usually due to a configuration change + // that needs a restart to get active. If this is the case, do not try + // to use SecureUserConfig to safe this still untested new configuration + } + else + { + // Test if SecureUserConfig is active. If yes and we are at this point, regular shutdown + // is in progress and the currently used configuration was working. Try to secure this + // working configuration for later eventually necessary restores + comphelper::BackupFileHelper aBackupFileHelper; + + aBackupFileHelper.tryPush(); + aBackupFileHelper.tryPushExtensionInfo(); + } + + // The acceptors in the AcceptorMap must be released (in DeregisterServices) + // with the solar mutex unlocked, to avoid deadlock: + { + SolarMutexReleaser aReleaser; + DeregisterServices(); +#if HAVE_FEATURE_SCRIPTING + StarBASIC::DetachAllDocBasicItems(); +#endif + } + + // be sure that path/language options gets destroyed before + // UCB is deinitialized + pExecGlobals->pCTLLanguageOptions.reset(); + pExecGlobals->pPathOptions.reset(); + + comphelper::ThreadPool::getSharedOptimalPool().shutdown(); + + bool bRR = pExecGlobals->bRestartRequested; + delete pExecGlobals; + pExecGlobals = nullptr; + + if ( bRR ) + { + restartOnMac(true); +#if !ENABLE_WASM_STRIP_SPLASH + if ( m_rSplashScreen.is() ) + m_rSplashScreen->reset(); +#endif + + return EXITHELPER_NORMAL_RESTART; + } + return EXIT_SUCCESS; +} + +IMPL_STATIC_LINK( Desktop, ImplInitFilterHdl, ::ConvertData&, rData, bool ) +{ + return GraphicFilter::GetGraphicFilter().GetFilterCallback().Call( rData ); +} + +bool Desktop::InitializeConfiguration() +{ + try + { + css::configuration::theDefaultProvider::get( + comphelper::getProcessComponentContext() ); + return true; + } + catch( css::lang::ServiceNotRegisteredException & e ) + { + HandleBootstrapErrors( + Desktop::BE_UNO_SERVICE_CONFIG_MISSING, e.Message ); + } + catch( const css::configuration::MissingBootstrapFileException& e ) + { + OUString aMsg( CreateErrorMsgString( utl::Bootstrap::MISSING_BOOTSTRAP_FILE, + e.BootstrapFileURL )); + HandleBootstrapPathErrors( ::utl::Bootstrap::INVALID_USER_INSTALL, aMsg ); + } + catch( const css::configuration::InvalidBootstrapFileException& e ) + { + OUString aMsg( CreateErrorMsgString( utl::Bootstrap::INVALID_BOOTSTRAP_FILE_ENTRY, + e.BootstrapFileURL )); + HandleBootstrapPathErrors( ::utl::Bootstrap::INVALID_BASE_INSTALL, aMsg ); + } + catch( const css::configuration::InstallationIncompleteException& ) + { + OUString aVersionFileURL; + OUString aMsg; + utl::Bootstrap::PathStatus aPathStatus = utl::Bootstrap::locateVersionFile( aVersionFileURL ); + if ( aPathStatus == utl::Bootstrap::PATH_EXISTS ) + aMsg = CreateErrorMsgString( utl::Bootstrap::MISSING_VERSION_FILE_ENTRY, aVersionFileURL ); + else + aMsg = CreateErrorMsgString( utl::Bootstrap::MISSING_VERSION_FILE, aVersionFileURL ); + + HandleBootstrapPathErrors( ::utl::Bootstrap::MISSING_USER_INSTALL, aMsg ); + } + catch ( const css::configuration::backend::BackendAccessException& exception) + { + // [cm122549] It is assumed in this case that the message + // coming from InitConfiguration (in fact CreateApplicationConf...) + // is suitable for display directly. + FatalError( MakeStartupErrorMessage( exception.Message ) ); + } + catch ( const css::configuration::backend::BackendSetupException& exception) + { + // [cm122549] It is assumed in this case that the message + // coming from InitConfiguration (in fact CreateApplicationConf...) + // is suitable for display directly. + FatalError( MakeStartupErrorMessage( exception.Message ) ); + } + catch ( const css::configuration::CannotLoadConfigurationException& ) + { + OUString aMsg( CreateErrorMsgString( utl::Bootstrap::INVALID_BOOTSTRAP_DATA, + OUString() )); + HandleBootstrapPathErrors( ::utl::Bootstrap::INVALID_BASE_INSTALL, aMsg ); + } + catch( const css::uno::Exception& ) + { + OUString aMsg( CreateErrorMsgString( utl::Bootstrap::INVALID_BOOTSTRAP_DATA, + OUString() )); + HandleBootstrapPathErrors( ::utl::Bootstrap::INVALID_BASE_INSTALL, aMsg ); + } + return false; +} + +void Desktop::FlushConfiguration() +{ + css::uno::Reference< css::util::XFlushable >( + css::configuration::theDefaultProvider::get( + comphelper::getProcessComponentContext()), + css::uno::UNO_QUERY_THROW)->flush(); +} + +bool Desktop::InitializeQuickstartMode( const Reference< XComponentContext >& rxContext ) +{ + try + { + // the shutdown icon sits in the systray and allows the user to keep + // the office instance running for quicker restart + // this will only be activated if --quickstart was specified on cmdline + + bool bQuickstart = shouldLaunchQuickstart(); + + // Try to instantiate quickstart service. This service is not mandatory, so + // do nothing if service is not available + + // #i105753# the following if was invented for performance + // unfortunately this broke the Mac behavior which is to always run + // in quickstart mode since Mac applications do not usually quit + // when the last document closes. + // Note that this claim that on macOS we "always run in quickstart mode" + // has nothing to do with (quick) *starting* (i.e. starting automatically + // when the user logs in), though, but with not quitting when no documents + // are open. + #ifndef MACOSX + if ( bQuickstart ) + #endif + { + css::office::Quickstart::createStart(rxContext, bQuickstart); + } + return true; + } + catch( const css::uno::Exception& ) + { + return false; + } +} + +void Desktop::OverrideSystemSettings( AllSettings& rSettings ) +{ + if ( !SvtTabAppearanceCfg::IsInitialized () ) + return; + + StyleSettings hStyleSettings = rSettings.GetStyleSettings(); + MouseSettings hMouseSettings = rSettings.GetMouseSettings(); + + DragFullOptions nDragFullOptions = hStyleSettings.GetDragFullOptions(); + + SvtTabAppearanceCfg aAppearanceCfg; + DragMode nDragMode = aAppearanceCfg.GetDragMode(); + switch ( nDragMode ) + { + case DragMode::FullWindow: + nDragFullOptions |= DragFullOptions::All; + break; + case DragMode::Frame: + nDragFullOptions &= ~DragFullOptions::All; + break; + case DragMode::SystemDep: + default: + break; + } + + MouseFollowFlags nFollow = hMouseSettings.GetFollow(); + hMouseSettings.SetFollow( aAppearanceCfg.IsMenuMouseFollow() ? (nFollow|MouseFollowFlags::Menu) : (nFollow&~MouseFollowFlags::Menu)); + rSettings.SetMouseSettings(hMouseSettings); + + bool bMenuIcons = officecfg::Office::Common::View::Menu::ShowIconsInMenues::get(); + bool bSystemMenuIcons = officecfg::Office::Common::View::Menu::IsSystemIconsInMenus::get(); + TriState eMenuIcons = bSystemMenuIcons ? TRISTATE_INDET : static_cast<TriState>(bMenuIcons); + hStyleSettings.SetUseImagesInMenus(eMenuIcons); + hStyleSettings.SetContextMenuShortcuts(static_cast<TriState>(officecfg::Office::Common::View::Menu::ShortcutsInContextMenus::get())); + hStyleSettings.SetDragFullOptions( nDragFullOptions ); + rSettings.SetStyleSettings ( hStyleSettings ); +} + +namespace { + +class ExitTimer : public Timer +{ + public: + ExitTimer() : Timer("desktop ExitTimer") + { + SetTimeout(500); + Start(); + } + virtual void Invoke() override + { + _exit(42); + } +}; + +} + +IMPL_LINK_NOARG(Desktop, OpenClients_Impl, void*, void) +{ + // #i114963# + // Enable IPC thread before OpenClients + // + // This is because it is possible for another client to connect during the OpenClients() call. + // This can happen on Windows when document is printed (not opened) and another client wants to print (when printing multiple documents). + // If the IPC thread is enabled after OpenClients, then the client will not be processed because the application will exit after printing. i.e RequestHandler::AreRequestsPending() will always return false + // + // ALSO: + // + // Multiple clients may request simultaneous connections. + // When this server closes down it attempts to recreate the pipe (in RequestHandler::Disable()). + // It's possible that the client has a pending connection request. + // When the IPC thread is not running, this connection locks (because maPipe.accept()) is never called + RequestHandler::SetReady(true); + OpenClients(); + + CloseSplashScreen(); + CheckFirstRun( ); +#ifdef _WIN32 + bool bDontShowDialogs + = Application::IsHeadlessModeEnabled(); // uitest.uicheck fails when the dialog is open + for (sal_uInt16 i = 0; !bDontShowDialogs && i < Application::GetCommandLineParamCount(); i++) + { + if (Application::GetCommandLineParam(i) == "--nologo") + bDontShowDialogs = true; + } + if (!bDontShowDialogs) + vcl::fileregistration::CheckFileExtRegistration(SfxGetpApp()->GetTopWindow()); + // Registers a COM class factory of the service manager with the windows operating system. + Reference< XMultiServiceFactory > xSMgr= comphelper::getProcessServiceFactory(); + xSMgr->createInstance("com.sun.star.bridge.OleApplicationRegistration"); + xSMgr->createInstance("com.sun.star.comp.ole.EmbedServer"); +#endif + const char *pExitPostStartup = getenv ("OOO_EXIT_POST_STARTUP"); + if (pExitPostStartup && *pExitPostStartup) + new ExitTimer(); +} + +void Desktop::OpenClients() +{ + + const CommandLineArgs& rArgs = GetCommandLineArgs(); + + if (!rArgs.IsQuickstart()) + { + OUString aHelpModule; + if (rArgs.IsHelpWriter()) { + aHelpModule = "swriter"; + } else if (rArgs.IsHelpCalc()) { + aHelpModule = "scalc"; + } else if (rArgs.IsHelpDraw()) { + aHelpModule = "sdraw"; + } else if (rArgs.IsHelpImpress()) { + aHelpModule = "simpress"; + } else if (rArgs.IsHelpBase()) { + aHelpModule = "sdatabase"; + } else if (rArgs.IsHelpBasic()) { + aHelpModule = "sbasic"; + } else if (rArgs.IsHelpMath()) { + aHelpModule = "smath"; + } + if (!aHelpModule.isEmpty()) { + OUString aHelpURL = "vnd.sun.star.help://" + + aHelpModule + + "/start?Language=" + + utl::ConfigManager::getUILocale(); +#if defined UNX + aHelpURL += "&System=UNX"; +#elif defined _WIN32 + aHelpURL += "&System=WIN"; +#endif + Application::GetHelp()->Start(aHelpURL); + return; + } + } + + // Disable AutoSave feature in case "--norestore" or a similar command line switch is set on the command line. + // The reason behind: AutoSave/EmergencySave/AutoRecovery share the same data. + // But the require that all documents, which are saved as backup should exists inside + // memory. May be this mechanism will be inconsistent if the configuration exists... + // but no document inside memory corresponds to this data. + // Further it's not acceptable to recover such documents without any UI. It can + // need some time, where the user won't see any results and wait for finishing the office startup... + bool bAllowRecoveryAndSessionManagement = ( !rArgs.IsNoRestore() ) && ( !rArgs.IsHeadless() ); + +#if !defined ANDROID + // Enter safe mode if requested + if (Application::IsSafeModeEnabled()) { + handleSafeMode(); + } +#endif + +#if HAVE_FEATURE_BREAKPAD + if (officecfg::Office::Common::Misc::CrashReport::get() && CrashReporter::crashReportInfoExists()) + handleCrashReport(); +#endif + + if ( ! bAllowRecoveryAndSessionManagement ) + { + try + { + Reference< XDispatch > xRecovery = css::frame::theAutoRecovery::get( ::comphelper::getProcessComponentContext() ); + Reference< css::util::XURLTransformer > xParser = css::util::URLTransformer::create( ::comphelper::getProcessComponentContext() ); + + css::util::URL aCmd; + aCmd.Complete = "vnd.sun.star.autorecovery:/disableRecovery"; + xParser->parseStrict(aCmd); + + xRecovery->dispatch(aCmd, css::uno::Sequence< css::beans::PropertyValue >()); + } + catch(const css::uno::Exception&) + { + TOOLS_WARN_EXCEPTION( "desktop.app", "Could not disable AutoRecovery."); + } + } + else + { + bool bExistsRecoveryData = false; +#if !ENABLE_WASM_STRIP_RECOVERYUI + bool bCrashed = false; + bool bExistsSessionData = false; + bool const bDisableRecovery + = getenv("OOO_DISABLE_RECOVERY") != nullptr + || IsOnSystemEventLoop() + || !officecfg::Office::Recovery::RecoveryInfo::Enabled::get(); + + impl_checkRecoveryState(bCrashed, bExistsRecoveryData, bExistsSessionData); + + if ( !bDisableRecovery && + ( + bExistsRecoveryData || // => crash with files => recovery + bCrashed // => crash without files => error report + ) + ) + { + try + { + impl_callRecoveryUI( + false , // false => force recovery instead of emergency save + bExistsRecoveryData); + } + catch(const css::uno::Exception&) + { + TOOLS_WARN_EXCEPTION( "desktop.app", "Error during recovery"); + } + } +#endif + + Reference< XSessionManagerListener2 > xSessionListener; + try + { + // specifies whether the UI-interaction on Session shutdown is allowed + bool bUIOnSessionShutdownAllowed = officecfg::Office::Recovery::SessionShutdown::DocumentStoreUIEnabled::get(); + xSessionListener = SessionListener::createWithOnQuitFlag( + ::comphelper::getProcessComponentContext(), bUIOnSessionShutdownAllowed); + } + catch(const css::uno::Exception&) + { + TOOLS_WARN_EXCEPTION( "desktop.app", "Registration of session listener failed"); + } + + if ( !bExistsRecoveryData && xSessionListener.is() ) + { + // session management + try + { + xSessionListener->doRestore(); + } + catch(const css::uno::Exception&) + { + TOOLS_WARN_EXCEPTION( "desktop.app", "Error in session management"); + } + } + } + + // write this information here to avoid depending on vcl in the crash reporter lib + CrashReporter::addKeyValue("Language", Application::GetSettings().GetLanguageTag().getBcp47(), CrashReporter::Create); + + RequestHandler::EnableRequests(); + + ProcessDocumentsRequest aRequest(rArgs.getCwdUrl()); + aRequest.aOpenList = rArgs.GetOpenList(); + aRequest.aViewList = rArgs.GetViewList(); + aRequest.aStartList = rArgs.GetStartList(); + aRequest.aPrintList = rArgs.GetPrintList(); + aRequest.aPrintToList = rArgs.GetPrintToList(); + aRequest.aPrinterName = rArgs.GetPrinterName(); + aRequest.aForceOpenList = rArgs.GetForceOpenList(); + aRequest.aForceNewList = rArgs.GetForceNewList(); + aRequest.aConversionList = rArgs.GetConversionList(); + aRequest.aConversionParams = rArgs.GetConversionParams(); + aRequest.aConversionOut = rArgs.GetConversionOut(); + aRequest.aImageConversionType = rArgs.GetImageConversionType(); + aRequest.aInFilter = rArgs.GetInFilter(); + aRequest.bTextCat = rArgs.IsTextCat(); + aRequest.bScriptCat = rArgs.IsScriptCat(); + + if ( !aRequest.aOpenList.empty() || + !aRequest.aViewList.empty() || + !aRequest.aStartList.empty() || + !aRequest.aPrintList.empty() || + !aRequest.aForceOpenList.empty() || + !aRequest.aForceNewList.empty() || + ( !aRequest.aPrintToList.empty() && !aRequest.aPrinterName.isEmpty() ) || + !aRequest.aConversionList.empty() ) + { + if ( rArgs.HasModuleParam() ) + { + SvtModuleOptions aOpt; + + // Support command line parameters to start a module (as preselection) + if ( rArgs.IsWriter() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::WRITER ) ) + aRequest.aModule = aOpt.GetFactoryName( SvtModuleOptions::EFactory::WRITER ); + else if ( rArgs.IsCalc() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::CALC ) ) + aRequest.aModule = aOpt.GetFactoryName( SvtModuleOptions::EFactory::CALC ); + else if ( rArgs.IsImpress() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::IMPRESS ) ) + aRequest.aModule= aOpt.GetFactoryName( SvtModuleOptions::EFactory::IMPRESS ); + else if ( rArgs.IsDraw() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::DRAW ) ) + aRequest.aModule= aOpt.GetFactoryName( SvtModuleOptions::EFactory::DRAW ); + } + + // check for printing disabled + if( ( !(aRequest.aPrintList.empty() && aRequest.aPrintToList.empty()) ) + && Application::GetSettings().GetMiscSettings().GetDisablePrinting() ) + { + aRequest.aPrintList.clear(); + aRequest.aPrintToList.clear(); + std::unique_ptr<weld::MessageDialog> xBox(Application::CreateMessageDialog(nullptr, + VclMessageType::Warning, VclButtonsType::Ok, + DpResId(STR_ERR_PRINTDISABLED))); + xBox->run(); + } + + // Process request + if ( RequestHandler::ExecuteCmdLineRequests(aRequest, false) ) + { + // Don't do anything if we have successfully called terminate at desktop: + return; + } + } + + // no default document if a document was loaded by recovery or by command line or if soffice is used as server + Reference< XDesktop2 > xDesktop = css::frame::Desktop::create( ::comphelper::getProcessComponentContext() ); + Reference< XElementAccess > xList( xDesktop->getFrames(), UNO_QUERY_THROW ); + if ( xList->hasElements() ) + return; + + if ( rArgs.IsQuickstart() || rArgs.IsInvisible() || Application::AnyInput( VclInputFlags::APPEVENT ) ) + // soffice was started as tray icon ... + return; + + OpenDefault(); +} + +void Desktop::OpenDefault() +{ + OUString aName; + SvtModuleOptions aOpt; + + const CommandLineArgs& rArgs = GetCommandLineArgs(); + if ( rArgs.IsNoDefault() ) return; + if ( rArgs.HasModuleParam() ) + { + // Support new command line parameters to start a module + if ( rArgs.IsWriter() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::WRITER ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::WRITER ); + else if ( rArgs.IsCalc() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::CALC ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::CALC ); + else if ( rArgs.IsImpress() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::IMPRESS ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::IMPRESS ); + else if ( rArgs.IsBase() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::DATABASE ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::DATABASE ); + else if ( rArgs.IsDraw() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::DRAW ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::DRAW ); + else if ( rArgs.IsMath() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::MATH ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::MATH ); + else if ( rArgs.IsGlobal() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::WRITER ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::WRITERGLOBAL ); + else if ( rArgs.IsWeb() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::WRITER ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::WRITERWEB ); + } + + if ( aName.isEmpty() ) + { + if (aOpt.IsModuleInstalled(SvtModuleOptions::EModule::STARTMODULE)) + { + ShowBackingComponent(nullptr); + return; + } + + // Old way to create a default document + if ( aOpt.IsModuleInstalled( SvtModuleOptions::EModule::WRITER ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::WRITER ); + else if ( aOpt.IsModuleInstalled( SvtModuleOptions::EModule::CALC ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::CALC ); + else if ( aOpt.IsModuleInstalled( SvtModuleOptions::EModule::IMPRESS ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::IMPRESS ); + else if ( aOpt.IsModuleInstalled( SvtModuleOptions::EModule::DATABASE ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::DATABASE ); + else if ( aOpt.IsModuleInstalled( SvtModuleOptions::EModule::DRAW ) ) + aName = aOpt.GetFactoryEmptyDocumentURL( SvtModuleOptions::EFactory::DRAW ); + else + return; + } + + ProcessDocumentsRequest aRequest(rArgs.getCwdUrl()); + aRequest.aOpenList.push_back(aName); + RequestHandler::ExecuteCmdLineRequests(aRequest, false); +} + + +OUString GetURL_Impl( + const OUString& rName, std::optional< OUString > const & cwdUrl ) +{ + // if rName is a vnd.sun.star.script URL do not attempt to parse it + // as INetURLObj does not handle URLs there + if (rName.startsWith("vnd.sun.star.script")) + { + return rName; + } + + // don't touch file urls, those should already be in internal form + // they won't get better here (#112849#) + if (comphelper::isFileUrl(rName)) + { + return rName; + } + + if ( rName.startsWith("service:")) + { + return rName; + } + + // Add path separator to these directory and make given URL (rName) absolute by using of current working directory + // Attention: "setFinalSlash()" is necessary for calling "smartRel2Abs()"!!! + // Otherwise last part will be ignored and wrong result will be returned!!! + // "smartRel2Abs()" interpret given URL as file not as path. So he truncate last element to get the base path ... + // But if we add a separator - he doesn't do it anymore. + INetURLObject aObj; + if (cwdUrl) { + aObj.SetURL(*cwdUrl); + aObj.setFinalSlash(); + } + + // Use the provided parameters for smartRel2Abs to support the usage of '%' in system paths. + // Otherwise this char won't get encoded and we are not able to load such files later, + bool bWasAbsolute; + INetURLObject aURL = aObj.smartRel2Abs( rName, bWasAbsolute, false, INetURLObject::EncodeMechanism::WasEncoded, + RTL_TEXTENCODING_UTF8, true ); + OUString aFileURL = aURL.GetMainURL(INetURLObject::DecodeMechanism::NONE); + + ::osl::FileStatus aStatus( osl_FileStatus_Mask_FileURL ); + ::osl::DirectoryItem aItem; + if( ::osl::FileBase::E_None == ::osl::DirectoryItem::get( aFileURL, aItem ) && + ::osl::FileBase::E_None == aItem.getFileStatus( aStatus ) ) + aFileURL = aStatus.getFileURL(); + + return aFileURL; +} + +void Desktop::HandleAppEvent( const ApplicationEvent& rAppEvent ) +{ + switch ( rAppEvent.GetEvent() ) + { + case ApplicationEvent::Type::Accept: + // every time an accept parameter is used we create an acceptor + // with the corresponding accept-string + createAcceptor(rAppEvent.GetStringData()); + break; + case ApplicationEvent::Type::Appear: + if ( !GetCommandLineArgs().IsInvisible() && !impl_bringToFrontRecoveryUI() ) + { + Reference< css::uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + + // find active task - the active task is always a visible task + Reference< css::frame::XDesktop2 > xDesktop = css::frame::Desktop::create( xContext ); + Reference< css::frame::XFrame > xTask = xDesktop->getActiveFrame(); + if ( !xTask.is() ) + { + // get any task if there is no active one + Reference< css::container::XIndexAccess > xList = xDesktop->getFrames(); + if ( xList->getCount() > 0 ) + xList->getByIndex(0) >>= xTask; + } + + if ( xTask.is() ) + { + Reference< css::awt::XTopWindow > xTop( xTask->getContainerWindow(), UNO_QUERY ); + xTop->toFront(); + } + else + { + // no visible task that could be activated found + Reference< css::awt::XWindow > xContainerWindow; + Reference< XFrame > xBackingFrame = xDesktop->findFrame( "_blank", 0); + if (xBackingFrame.is()) + xContainerWindow = xBackingFrame->getContainerWindow(); + if (xContainerWindow.is()) + { + Reference< XController > xStartModule = StartModule::createWithParentWindow(xContext, xContainerWindow); + Reference< css::awt::XWindow > xBackingWin(xStartModule, UNO_QUERY); + // Attention: You MUST(!) call setComponent() before you call attachFrame(). + // Because the backing component set the property "IsBackingMode" of the frame + // to true inside attachFrame(). But setComponent() reset this state every time ... + xBackingFrame->setComponent(xBackingWin, xStartModule); + xStartModule->attachFrame(xBackingFrame); + xContainerWindow->setVisible(true); + + VclPtr<vcl::Window> pCompWindow = VCLUnoHelper::GetWindow(xBackingFrame->getComponentWindow()); + if (pCompWindow) + pCompWindow->PaintImmediately(); + } + } + } + break; + case ApplicationEvent::Type::Open: + { + const CommandLineArgs& rCmdLine = GetCommandLineArgs(); + if ( !rCmdLine.IsInvisible() && !rCmdLine.IsTerminateAfterInit() ) + { + ProcessDocumentsRequest docsRequest(rCmdLine.getCwdUrl()); + std::vector<OUString> const & data(rAppEvent.GetStringsData()); + docsRequest.aOpenList.insert( + docsRequest.aOpenList.end(), data.begin(), data.end()); + RequestHandler::ExecuteCmdLineRequests(docsRequest, false); + } + } + break; + case ApplicationEvent::Type::OpenHelpUrl: + // start help for a specific URL + Application::GetHelp()->Start(rAppEvent.GetStringData()); + break; + case ApplicationEvent::Type::Print: + { + const CommandLineArgs& rCmdLine = GetCommandLineArgs(); + if ( !rCmdLine.IsInvisible() && !rCmdLine.IsTerminateAfterInit() ) + { + ProcessDocumentsRequest docsRequest(rCmdLine.getCwdUrl()); + std::vector<OUString> const & data(rAppEvent.GetStringsData()); + docsRequest.aPrintList.insert( + docsRequest.aPrintList.end(), data.begin(), data.end()); + RequestHandler::ExecuteCmdLineRequests(docsRequest, false); + } + } + break; + case ApplicationEvent::Type::PrivateDoShutdown: + { + Desktop* pD = dynamic_cast<Desktop*>(GetpApp()); + OSL_ENSURE( pD, "no desktop ?!?" ); + if( pD ) + pD->doShutdown(); + } + break; + case ApplicationEvent::Type::QuickStart: + if ( !GetCommandLineArgs().IsInvisible() ) + { + // If the office has been started the second time its command line arguments are sent through a pipe + // connection to the first office. We want to reuse the quickstart option for the first office. + // NOTICE: The quickstart service must be initialized inside the "main thread", so we use the + // application events to do this (they are executed inside main thread)!!! + // Don't start quickstart service if the user specified "--invisible" on the command line! + Reference< css::uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + css::office::Quickstart::createStart(xContext, true/*Quickstart*/); + } + break; + case ApplicationEvent::Type::ShowDialog: + // This is only used on macOS, and only for About or Preferences. + // Ignore all errors here. It's clicking a menu entry only ... + // The user will try it again, in case nothing happens .-) + try + { + Reference< css::uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + + Reference< css::frame::XDesktop2 > xDesktop = css::frame::Desktop::create( xContext ); + + Reference< css::util::XURLTransformer > xParser = css::util::URLTransformer::create(xContext); + css::util::URL aCommand; + if( rAppEvent.GetStringData() == "PREFERENCES" ) + aCommand.Complete = ".uno:OptionsTreeDialog"; + else if( rAppEvent.GetStringData() == "ABOUT" ) + aCommand.Complete = ".uno:About"; + if( !aCommand.Complete.isEmpty() ) + { + xParser->parseStrict(aCommand); + + css::uno::Reference< css::frame::XDispatch > xDispatch = xDesktop->queryDispatch(aCommand, OUString(), 0); + if (xDispatch.is()) + xDispatch->dispatch(aCommand, css::uno::Sequence< css::beans::PropertyValue >()); + } + } + catch(const css::uno::Exception&) + { + TOOLS_WARN_EXCEPTION("desktop.app", "exception thrown by dialog"); + } + break; + case ApplicationEvent::Type::Unaccept: + // try to remove corresponding acceptor + destroyAcceptor(rAppEvent.GetStringData()); + break; + default: + SAL_WARN( "desktop.app", "this cannot happen"); + break; + } +} + +#if !ENABLE_WASM_STRIP_SPLASH +void Desktop::OpenSplashScreen() +{ + const CommandLineArgs &rCmdLine = GetCommandLineArgs(); + // Show intro only if this is normal start (e.g. no server, no quickstart, no printing ) + if ( !(!rCmdLine.IsInvisible() && + !rCmdLine.IsHeadless() && + !rCmdLine.IsQuickstart() && + !rCmdLine.IsMinimized() && + !rCmdLine.IsNoLogo() && + !rCmdLine.IsTerminateAfterInit() && + rCmdLine.GetPrintList().empty() && + rCmdLine.GetPrintToList().empty() && + rCmdLine.GetConversionList().empty()) ) + return; + + // Determine application name from command line parameters + OUString aAppName; + if ( rCmdLine.IsWriter() ) + aAppName = "writer"; + else if ( rCmdLine.IsCalc() ) + aAppName = "calc"; + else if ( rCmdLine.IsDraw() ) + aAppName = "draw"; + else if ( rCmdLine.IsImpress() ) + aAppName = "impress"; + else if ( rCmdLine.IsBase() ) + aAppName = "base"; + else if ( rCmdLine.IsGlobal() ) + aAppName = "global"; + else if ( rCmdLine.IsMath() ) + aAppName = "math"; + else if ( rCmdLine.IsWeb() ) + aAppName = "web"; + + // Which splash to use + OUString aSplashService( "com.sun.star.office.SplashScreen" ); + if ( rCmdLine.HasSplashPipe() ) + aSplashService = "com.sun.star.office.PipeSplashScreen"; + + Sequence< Any > aSeq{ Any(true) /* bVisible */, Any(aAppName) }; + css::uno::Reference< css::uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + m_rSplashScreen.set( + xContext->getServiceManager()->createInstanceWithArgumentsAndContext(aSplashService, aSeq, xContext), + UNO_QUERY); + + if(m_rSplashScreen.is()) + m_rSplashScreen->start("SplashScreen", 100); + +} +#endif + +void Desktop::SetSplashScreenProgress(sal_Int32 iProgress) +{ +#if ENABLE_WASM_STRIP_SPLASH + (void) iProgress; +#else + if(m_rSplashScreen.is()) + { + m_rSplashScreen->setValue(iProgress); + } +#endif +} + +void Desktop::SetSplashScreenText( const OUString& rText ) +{ +#if ENABLE_WASM_STRIP_SPLASH + (void) rText; +#else + if( m_rSplashScreen.is() ) + { + m_rSplashScreen->setText( rText ); + } +#endif +} + +void Desktop::CloseSplashScreen() +{ +#if !ENABLE_WASM_STRIP_SPLASH + if(m_rSplashScreen.is()) + { + SolarMutexGuard ensureSolarMutex; + m_rSplashScreen->end(); + m_rSplashScreen = nullptr; + } +#endif +} + + +IMPL_STATIC_LINK_NOARG(Desktop, AsyncInitFirstRun, Timer *, void) +{ + // does initializations which are necessary for the first run of the office + try + { + Reference< XJobExecutor > xExecutor = theJobExecutor::get( ::comphelper::getProcessComponentContext() ); + xExecutor->trigger( "onFirstRunInitialization" ); + } + catch(const css::uno::Exception&) + { + TOOLS_WARN_EXCEPTION( "desktop.app", "Desktop::DoFirstRunInitializations: caught an exception while trigger job executor" ); + } +} + +void Desktop::ShowBackingComponent(Desktop * progress) +{ + if (GetCommandLineArgs().IsNoDefault()) + { + return; + } + Reference< XComponentContext > xContext = comphelper::getProcessComponentContext(); + Reference< XDesktop2 > xDesktop = css::frame::Desktop::create(xContext); + if (progress != nullptr) + { + progress->SetSplashScreenProgress(60); + } + Reference< XFrame > xBackingFrame = xDesktop->findFrame( "_blank", 0); + Reference< css::awt::XWindow > xContainerWindow; + + if (xBackingFrame.is()) + xContainerWindow = xBackingFrame->getContainerWindow(); + if (!xContainerWindow.is()) + return; + + // set the WindowExtendedStyle::Document style. Normally, this is done by the TaskCreator service when a "_blank" + // frame/window is created. Since we do not use the TaskCreator here, we need to mimic its behavior, + // otherwise documents loaded into this frame will later on miss functionality depending on the style. + VclPtr<vcl::Window> pContainerWindow = VCLUnoHelper::GetWindow( xContainerWindow ); + SAL_WARN_IF( !pContainerWindow, "desktop.app", "Desktop::Main: no implementation access to the frame's container window!" ); + pContainerWindow->SetExtendedStyle( pContainerWindow->GetExtendedStyle() | WindowExtendedStyle::Document ); + if (progress != nullptr) + { + progress->SetSplashScreenProgress(75); + } + + Reference< XController > xStartModule = StartModule::createWithParentWindow( xContext, xContainerWindow); + // Attention: You MUST(!) call setComponent() before you call attachFrame(). + // Because the backing component set the property "IsBackingMode" of the frame + // to true inside attachFrame(). But setComponent() reset this state everytimes ... + xBackingFrame->setComponent(Reference< XWindow >(xStartModule, UNO_QUERY), xStartModule); + if (progress != nullptr) + { + progress->SetSplashScreenProgress(100); + } + xStartModule->attachFrame(xBackingFrame); + if (progress != nullptr) + { + progress->CloseSplashScreen(); + } + xContainerWindow->setVisible(true); +} + + +void Desktop::CheckFirstRun( ) +{ + if (!officecfg::Office::Common::Misc::FirstRun::get()) + return; + + // use VCL timer, which won't trigger during shutdown if the + // application exits before timeout + m_firstRunTimer.Start(); + +#ifdef _WIN32 + // Check if Quickstarter should be started (on Windows only) + OUString sRootKey = ReplaceStringHookProc("Software\\%OOOVENDOR\\%PRODUCTNAME\\%PRODUCTVERSION"); + WCHAR szValue[8192]; + DWORD nValueSize = sizeof(szValue); + HKEY hKey; + if (ERROR_SUCCESS == RegOpenKeyW(HKEY_LOCAL_MACHINE, o3tl::toW(sRootKey.getStr()), &hKey)) + { + if ( ERROR_SUCCESS == RegQueryValueExW( hKey, L"RunQuickstartAtFirstStart", nullptr, nullptr, reinterpret_cast<LPBYTE>(szValue), &nValueSize ) ) + { + css::uno::Reference< css::uno::XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + css::office::Quickstart::createAutoStart(xContext, true/*Quickstart*/, true/*bAutostart*/); + RegCloseKey( hKey ); + } + } +#endif + + std::shared_ptr< comphelper::ConfigurationChanges > batch( + comphelper::ConfigurationChanges::create()); + officecfg::Office::Common::Misc::FirstRun::set(false, batch); + batch->commit(); +} + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/appinit.cxx b/desktop/source/app/appinit.cxx new file mode 100644 index 000000000..a70004e23 --- /dev/null +++ b/desktop/source/app/appinit.cxx @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <algorithm> + +#include <app.hxx> +#include <dp_shared.hxx> +#include "cmdlineargs.hxx" +#include <strings.hrc> +#include <com/sun/star/registry/XSimpleRegistry.hpp> +#include <com/sun/star/lang/XInitialization.hpp> +#include <com/sun/star/lang/XMultiServiceFactory.hpp> +#include <com/sun/star/uno/Exception.hpp> +#include <com/sun/star/ucb/UniversalContentBroker.hpp> +#include <cppuhelper/bootstrap.hxx> +#include <officecfg/Setup.hxx> +#include <osl/file.hxx> +#include <rtl/bootstrap.hxx> +#include <sal/log.hxx> +#include <tools/diagnose_ex.h> + +#include <comphelper/processfactory.hxx> +#include <unotools/ucbhelper.hxx> +#include <unotools/tempfile.hxx> +#include <vcl/svapp.hxx> +#include <unotools/pathoptions.hxx> +#include <map> + +using namespace desktop; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::registry; +using namespace ::com::sun::star::ucb; + +namespace desktop +{ + + +static void configureUcb() +{ + // For backwards compatibility, in case some code still uses plain + // createInstance w/o args directly to obtain an instance: + UniversalContentBroker::create(comphelper::getProcessComponentContext()); +} + +void Desktop::InitApplicationServiceManager() +{ + Reference<XMultiServiceFactory> sm; +#ifdef ANDROID + OUString aUnoRc( "file:///assets/program/unorc" ); + sm.set( + cppu::defaultBootstrap_InitialComponentContext( aUnoRc )->getServiceManager(), + UNO_QUERY_THROW); +#elif defined(IOS) + OUString uri( "$APP_DATA_DIR" ); + rtl_bootstrap_expandMacros( &uri.pData ); + OUString aUnoRc("file://" + uri + "/unorc"); + sm.set( + cppu::defaultBootstrap_InitialComponentContext( aUnoRc )->getServiceManager(), + UNO_QUERY_THROW); +#else + sm.set( + cppu::defaultBootstrap_InitialComponentContext()->getServiceManager(), + UNO_QUERY_THROW); +#endif + comphelper::setProcessServiceFactory(sm); +} + +void Desktop::RegisterServices() +{ + if( m_bServicesRegistered ) + return; + + // interpret command line arguments + CommandLineArgs& rCmdLine = GetCommandLineArgs(); + + // Headless mode for FAT Office, auto cancels any dialogs that popup + if (rCmdLine.IsEventTesting()) + Application::EnableEventTestingMode(); + else if (rCmdLine.IsHeadless()) + Application::EnableHeadlessMode(false); + + // read accept string from configuration + OUString conDcpCfg( + officecfg::Setup::Office::ooSetupConnectionURL::get()); + if (!conDcpCfg.isEmpty()) { + createAcceptor(conDcpCfg); + } + + std::vector< OUString > const & conDcp = rCmdLine.GetAccept(); + for (auto const& elem : conDcp) + { + createAcceptor(elem); + } + + configureUcb(); + + CreateTemporaryDirectory(); + m_bServicesRegistered = true; +} + +typedef std::map< OUString, css::uno::Reference<css::lang::XInitialization> > AcceptorMap; + +namespace +{ + AcceptorMap& acceptorMap() + { + static AcceptorMap SINGLETON; + return SINGLETON; + } + OUString& CurrentTempURL() + { + static OUString SINGLETON; + return SINGLETON; + } +} + +static bool bAccept = false; + +void Desktop::createAcceptor(const OUString& aAcceptString) +{ + // check whether the requested acceptor already exists + AcceptorMap &rMap = acceptorMap(); + AcceptorMap::const_iterator pIter = rMap.find(aAcceptString); + if (pIter != rMap.end() ) + return; + + Sequence< Any > aSeq{ Any(aAcceptString), Any(bAccept) }; + Reference< XComponentContext > xContext = ::comphelper::getProcessComponentContext(); + Reference<XInitialization> rAcceptor( + xContext->getServiceManager()->createInstanceWithContext("com.sun.star.office.Acceptor", xContext), + UNO_QUERY ); + if ( rAcceptor.is() ) + { + try + { + rAcceptor->initialize( aSeq ); + rMap.emplace(aAcceptString, rAcceptor); + } + catch (const css::uno::Exception&) + { + // no error handling needed... + // acceptor just won't come up + TOOLS_WARN_EXCEPTION( "desktop.app", "Acceptor could not be created"); + } + } + else + { + // there is already an acceptor with this description + SAL_WARN( "desktop.app", "Acceptor already exists."); + } +} + +namespace { + +class enable +{ + private: + Sequence<Any> m_aSeq{ Any(true) }; + public: + enable() = default; + void operator() (const AcceptorMap::value_type& val) { + if (val.second.is()) { + val.second->initialize(m_aSeq); + } + } +}; + +} + +// enable acceptors +IMPL_STATIC_LINK_NOARG(Desktop, EnableAcceptors_Impl, void*, void) +{ + if (!bAccept) + { + // from now on, all new acceptors are enabled + bAccept = true; + // enable existing acceptors by calling initialize(true) + // on all existing acceptors + AcceptorMap &rMap = acceptorMap(); + std::for_each(rMap.begin(), rMap.end(), enable()); + } +} + +void Desktop::destroyAcceptor(const OUString& aAcceptString) +{ + // special case stop all acceptors + AcceptorMap &rMap = acceptorMap(); + if (aAcceptString == "all") { + rMap.clear(); + + } else { + // try to remove acceptor from map + AcceptorMap::const_iterator pIter = rMap.find(aAcceptString); + if (pIter != rMap.end() ) { + // remove reference from map + // this is the last reference and the acceptor will be destructed + rMap.erase(aAcceptString); + } else { + SAL_WARN( "desktop.app", "Found no acceptor to remove"); + } + } +} + + +void Desktop::DeregisterServices() +{ + // stop all acceptors by clearing the map + acceptorMap().clear(); +} + +void Desktop::CreateTemporaryDirectory() +{ + OUString aTempBaseURL; + try + { + SvtPathOptions aOpt; + aTempBaseURL = aOpt.GetTempPath(); + } + catch (RuntimeException& e) + { + // Catch runtime exception here: We have to add language dependent info + // to the exception message. Fallback solution uses hard coded string. + OUString aMsg = DpResId(STR_BOOTSTRAP_ERR_NO_PATHSET_SERVICE); + e.Message = aMsg + e.Message; + throw; + } + + // create new current temporary directory + OUString aTempPath = ::utl::TempFile::SetTempNameBaseDirectory( aTempBaseURL ); + if ( aTempPath.isEmpty() + && ::osl::File::getTempDirURL( aTempBaseURL ) == osl::FileBase::E_None ) + { + aTempPath = ::utl::TempFile::SetTempNameBaseDirectory( aTempBaseURL ); + } + + // set new current temporary directory + OUString aRet; + if (osl::FileBase::getFileURLFromSystemPath( aTempPath, aRet ) + != osl::FileBase::E_None) + { + aRet.clear(); + } + CurrentTempURL() = aRet; +} + +void Desktop::RemoveTemporaryDirectory() +{ + // remove current temporary directory + OUString &rCurrentTempURL = CurrentTempURL(); + if ( !rCurrentTempURL.isEmpty() ) + { + ::utl::UCBContentHelper::Kill( rCurrentTempURL ); + } +} + +} // namespace desktop + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/check_ext_deps.cxx b/desktop/source/app/check_ext_deps.cxx new file mode 100644 index 000000000..712ed6c73 --- /dev/null +++ b/desktop/source/app/check_ext_deps.cxx @@ -0,0 +1,427 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <config_folders.h> +#include <config_features.h> + +#include <rtl/bootstrap.hxx> +#include <rtl/ustring.hxx> +#include <sal/log.hxx> +#include <cppuhelper/implbase.hxx> + +#include <tools/diagnose_ex.h> +#include <toolkit/helper/vclunohelper.hxx> + +#include <comphelper/lok.hxx> +#include <comphelper/processfactory.hxx> +#include <cppuhelper/exc_hlp.hxx> +#include <com/sun/star/lang/WrappedTargetRuntimeException.hpp> +#include <com/sun/star/ucb/CommandAbortedException.hpp> +#include <com/sun/star/ucb/CommandFailedException.hpp> +#include <com/sun/star/ucb/XCommandEnvironment.hpp> +#include <com/sun/star/beans/XPropertySet.hpp> +#include <com/sun/star/beans/NamedValue.hpp> +#include <com/sun/star/configuration/theDefaultProvider.hpp> +#include <com/sun/star/deployment/DeploymentException.hpp> +#include <com/sun/star/deployment/XPackage.hpp> +#include <com/sun/star/deployment/ExtensionManager.hpp> +#include <com/sun/star/deployment/LicenseException.hpp> +#include <com/sun/star/deployment/ui/LicenseDialog.hpp> +#include <com/sun/star/task/OfficeRestartManager.hpp> +#include <com/sun/star/task/XInteractionApprove.hpp> +#include <com/sun/star/task/XInteractionAbort.hpp> +#include <com/sun/star/ui/dialogs/XExecutableDialog.hpp> +#include <com/sun/star/ui/dialogs/ExecutableDialogResults.hpp> +#include <com/sun/star/util/XChangesBatch.hpp> + +#include <app.hxx> +#include <utility> + +#include <dp_misc.h> + +using namespace desktop; +using namespace com::sun::star; +using namespace com::sun::star::lang; +using namespace com::sun::star::task; +using namespace com::sun::star::uno; + +namespace +{ +//For use with XExtensionManager.synchronize +class SilentCommandEnv + : public ::cppu::WeakImplHelper< ucb::XCommandEnvironment, + task::XInteractionHandler, + ucb::XProgressHandler > +{ + uno::Reference<uno::XComponentContext> mxContext; + Desktop *mpDesktop; + sal_Int32 mnLevel; + sal_Int32 mnProgress; + +public: + SilentCommandEnv( + uno::Reference<uno::XComponentContext> xContext, + Desktop* pDesktop ); + virtual ~SilentCommandEnv() override; + + // XCommandEnvironment + virtual uno::Reference<task::XInteractionHandler > SAL_CALL + getInteractionHandler() override; + virtual uno::Reference<ucb::XProgressHandler > + SAL_CALL getProgressHandler() override; + + // XInteractionHandler + virtual void SAL_CALL handle( + uno::Reference<task::XInteractionRequest > const & xRequest ) override; + + // XProgressHandler + virtual void SAL_CALL push( uno::Any const & Status ) override; + virtual void SAL_CALL update( uno::Any const & Status ) override; + virtual void SAL_CALL pop() override; +}; + + +SilentCommandEnv::SilentCommandEnv( + uno::Reference<uno::XComponentContext> xContext, + Desktop* pDesktop ): + mxContext(std::move( xContext )), + mpDesktop( pDesktop ), + mnLevel( 0 ), + mnProgress( 25 ) +{} + + +SilentCommandEnv::~SilentCommandEnv() +{ + if (mpDesktop) + mpDesktop->SetSplashScreenText(OUString()); +} + + +Reference<task::XInteractionHandler> SilentCommandEnv::getInteractionHandler() +{ + return this; +} + + +Reference<ucb::XProgressHandler> SilentCommandEnv::getProgressHandler() +{ + return this; +} + + +// XInteractionHandler +void SilentCommandEnv::handle( Reference< task::XInteractionRequest> const & xRequest ) +{ + deployment::LicenseException licExc; + + uno::Any request( xRequest->getRequest() ); + bool bApprove = true; + + if ( request >>= licExc ) + { + uno::Reference< ui::dialogs::XExecutableDialog > xDialog( + deployment::ui::LicenseDialog::create( + mxContext, VCLUnoHelper::GetInterface( nullptr ), + licExc.ExtensionName, licExc.Text ) ); + sal_Int16 res = xDialog->execute(); + if ( res == ui::dialogs::ExecutableDialogResults::CANCEL ) + bApprove = false; + else if ( res == ui::dialogs::ExecutableDialogResults::OK ) + bApprove = true; + else + { + OSL_ASSERT(false); + } + } + + // We approve everything here + uno::Sequence< Reference< task::XInteractionContinuation > > conts( xRequest->getContinuations() ); + Reference< task::XInteractionContinuation > const * pConts = conts.getConstArray(); + sal_Int32 len = conts.getLength(); + + for ( sal_Int32 pos = 0; pos < len; ++pos ) + { + if ( bApprove ) + { + uno::Reference< task::XInteractionApprove > xInteractionApprove( pConts[ pos ], uno::UNO_QUERY ); + if ( xInteractionApprove.is() ) + xInteractionApprove->select(); + } + else + { + uno::Reference< task::XInteractionAbort > xInteractionAbort( pConts[ pos ], uno::UNO_QUERY ); + if ( xInteractionAbort.is() ) + xInteractionAbort->select(); + } + } +} + + +// XProgressHandler +void SilentCommandEnv::push( uno::Any const & rStatus ) +{ + OUString sText; + mnLevel += 1; + + if (mpDesktop && rStatus.hasValue() && (rStatus >>= sText)) + { + if ( mnLevel <= 3 ) + mpDesktop->SetSplashScreenText( sText ); + else + mpDesktop->SetSplashScreenProgress( ++mnProgress ); + } +} + + +void SilentCommandEnv::update( uno::Any const & rStatus ) +{ + OUString sText; + if (mpDesktop && rStatus.hasValue() && (rStatus >>= sText)) + { + mpDesktop->SetSplashScreenText( sText ); + } +} + + +void SilentCommandEnv::pop() +{ + mnLevel -= 1; +} + +} // end namespace + + +constexpr OUStringLiteral aAccessSrvc = u"com.sun.star.configuration.ConfigurationUpdateAccess"; + +static sal_Int16 impl_showExtensionDialog( uno::Reference< uno::XComponentContext > const &xContext ) +{ + uno::Reference< uno::XInterface > xService; + sal_Int16 nRet = 0; + + uno::Reference< lang::XMultiComponentFactory > xServiceManager( xContext->getServiceManager() ); + if( !xServiceManager.is() ) + throw uno::RuntimeException( + "impl_showExtensionDialog(): unable to obtain service manager from component context", uno::Reference< uno::XInterface > () ); + + xService = xServiceManager->createInstanceWithContext( "com.sun.star.deployment.ui.UpdateRequiredDialog", xContext ); + uno::Reference< ui::dialogs::XExecutableDialog > xExecutable( xService, uno::UNO_QUERY ); + if ( xExecutable.is() ) + nRet = xExecutable->execute(); + + return nRet; +} + + +// Check dependencies of all packages + +static bool impl_checkDependencies( const uno::Reference< uno::XComponentContext > &xContext ) +{ + uno::Sequence< uno::Sequence< uno::Reference< deployment::XPackage > > > xAllPackages; + uno::Reference< deployment::XExtensionManager > xExtensionManager = deployment::ExtensionManager::get( xContext ); + + if ( !xExtensionManager.is() ) + { + SAL_WARN( "desktop.app", "Could not get the Extension Manager!" ); + return true; + } + + try { + xAllPackages = xExtensionManager->getAllExtensions( uno::Reference< task::XAbortChannel >(), + uno::Reference< ucb::XCommandEnvironment >() ); + } + catch ( const deployment::DeploymentException & ) { return true; } + catch ( const ucb::CommandFailedException & ) { return true; } + catch ( const ucb::CommandAbortedException & ) { return true; } + catch ( const lang::IllegalArgumentException & e ) { + css::uno::Any anyEx = cppu::getCaughtException(); + throw css::lang::WrappedTargetRuntimeException( e.Message, + e.Context, anyEx ); + } + +#ifdef DEBUG + sal_Int32 const nMax = 3; +#else + sal_Int32 const nMax = 2; +#endif + + for ( uno::Sequence< uno::Reference< deployment::XPackage > > const & xPackageList : std::as_const(xAllPackages) ) + { + for ( sal_Int32 j = 0; (j<nMax) && (j < xPackageList.getLength()); ++j ) + { + uno::Reference< deployment::XPackage > xPackage = xPackageList[j]; + if ( xPackage.is() ) + { + bool bRegistered = false; + try { + beans::Optional< beans::Ambiguous< sal_Bool > > option( xPackage->isRegistered( uno::Reference< task::XAbortChannel >(), + uno::Reference< ucb::XCommandEnvironment >() ) ); + if ( option.IsPresent ) + { + ::beans::Ambiguous< sal_Bool > const & reg = option.Value; + if ( reg.IsAmbiguous ) + bRegistered = false; + else + bRegistered = reg.Value; + } + else + bRegistered = false; + } + catch ( const uno::RuntimeException & ) { throw; } + catch (const uno::Exception & ) { + TOOLS_WARN_EXCEPTION( "desktop.app", "" ); + } + + if ( bRegistered ) + { + bool bDependenciesValid = false; + try { + bDependenciesValid = xPackage->checkDependencies( uno::Reference< ucb::XCommandEnvironment >() ); + } + catch ( const deployment::DeploymentException & ) {} + if ( ! bDependenciesValid ) + { + return false; + } + } + } + } + } + return true; +} + + +// resets the 'check needed' flag (needed, if aborted) + +static void impl_setNeedsCompatCheck() +{ + try { + Reference< XMultiServiceFactory > theConfigProvider( + configuration::theDefaultProvider::get( + comphelper::getProcessComponentContext() ) ); + + beans::NamedValue v( "nodepath", + Any( OUString("org.openoffice.Setup/Office") ) ); + Sequence< Any > theArgs{ Any(v) }; + Reference< beans::XPropertySet > pset( + theConfigProvider->createInstanceWithArguments( aAccessSrvc, theArgs ), UNO_QUERY_THROW ); + + Any value( OUString("never") ); + + pset->setPropertyValue("LastCompatibilityCheckID", value ); + Reference< util::XChangesBatch >( pset, UNO_QUERY_THROW )->commitChanges(); + } + catch (const Exception&) {} +} + + +// to check if we need checking the dependencies of the extensions again, we compare +// the build id of the office with the one of the last check + +static bool impl_needsCompatCheck() +{ + bool bNeedsCheck = false; + OUString aLastCheckBuildID; + OUString aCurrentBuildID( "${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("version") ":buildid}" ); + rtl::Bootstrap::expandMacros( aCurrentBuildID ); + + try { + Reference< XMultiServiceFactory > theConfigProvider( + configuration::theDefaultProvider::get( + comphelper::getProcessComponentContext() ) ); + + beans::NamedValue v( "nodepath", + Any( OUString("org.openoffice.Setup/Office") ) ); + Sequence< Any > theArgs{ Any(v) }; + Reference< beans::XPropertySet > pset( + theConfigProvider->createInstanceWithArguments( aAccessSrvc, theArgs ), UNO_QUERY_THROW ); + + Any result = pset->getPropertyValue("LastCompatibilityCheckID"); + + result >>= aLastCheckBuildID; + if ( aLastCheckBuildID != aCurrentBuildID ) + { + bNeedsCheck = true; + result <<= aCurrentBuildID; + pset->setPropertyValue("LastCompatibilityCheckID", result ); + Reference< util::XChangesBatch >( pset, UNO_QUERY_THROW )->commitChanges(); + } +#ifdef DEBUG + bNeedsCheck = true; +#endif + } + catch (const css::uno::Exception&) {} + + return bNeedsCheck; +} + + +// Do we need to check the dependencies of the extensions? +// When there are unresolved issues, we can't continue with startup +bool Desktop::CheckExtensionDependencies() +{ + if (!impl_needsCompatCheck()) + { + return false; + } + + uno::Reference< uno::XComponentContext > xContext( + comphelper::getProcessComponentContext()); + + bool bDependenciesValid = impl_checkDependencies( xContext ); + + short nRet = 0; + + if ( !bDependenciesValid ) + nRet = impl_showExtensionDialog( xContext ); + + if ( nRet == -1 ) + { + impl_setNeedsCompatCheck(); + return true; + } + else + return false; +} + +void Desktop::SynchronizeExtensionRepositories(bool bCleanedExtensionCache, Desktop* pDesktop) +{ + uno::Reference< uno::XComponentContext > context( + comphelper::getProcessComponentContext()); + uno::Reference< ucb::XCommandEnvironment > silent( + new SilentCommandEnv(context, pDesktop)); + if (bCleanedExtensionCache) { + deployment::ExtensionManager::get(context)->reinstallDeployedExtensions( + true, "user", Reference<task::XAbortChannel>(), silent); +#if !HAVE_FEATURE_MACOSX_SANDBOX + if (!comphelper::LibreOfficeKit::isActive()) + task::OfficeRestartManager::get(context)->requestRestart( + silent->getInteractionHandler()); +#endif + } else { + // reinstallDeployedExtensions above already calls syncRepositories internally + + // Force syncing repositories on startup. There are cases where the extension + // registration becomes invalid which leads to extensions not starting up, although + // installed and active. Syncing extension repos on startup fixes that. + dp_misc::syncRepositories(/*force=*/true, silent); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/cmdlineargs.cxx b/desktop/source/app/cmdlineargs.cxx new file mode 100644 index 000000000..9f9251064 --- /dev/null +++ b/desktop/source/app/cmdlineargs.cxx @@ -0,0 +1,793 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <config_features.h> + +#if HAVE_FEATURE_MACOSX_SANDBOX +#include <premac.h> +#include <Foundation/Foundation.h> +#include <postmac.h> +#endif + +#include "cmdlineargs.hxx" +#include <osl/thread.hxx> +#include <tools/stream.hxx> +#include <tools/urlobj.hxx> +#include <rtl/ustring.hxx> +#include <rtl/process.h> +#include <comphelper/lok.hxx> +#include <comphelper/processfactory.hxx> +#include <com/sun/star/uri/ExternalUriReferenceTranslator.hpp> +#include <unotools/bootstrap.hxx> + +#include <rtl/strbuf.hxx> +#include <osl/file.hxx> +#include <sal/log.hxx> + +using namespace com::sun::star::lang; +using namespace com::sun::star::uri; +using namespace com::sun::star::uno; + +namespace desktop +{ + +namespace { + +OUString translateExternalUris(OUString const & input) { + OUString t( + css::uri::ExternalUriReferenceTranslator::create( + comphelper::getProcessComponentContext())-> + translateToInternal(input)); + return t.isEmpty() ? input : t; +} + +std::vector< OUString > translateExternalUris( + std::vector< OUString > const & input) +{ + std::vector< OUString > t; + t.reserve(input.size()); + for (auto const& elem : input) + { + t.push_back(translateExternalUris(elem)); + } + return t; +} + +class ExtCommandLineSupplier: public CommandLineArgs::Supplier { +public: + explicit ExtCommandLineSupplier(): + m_count( + comphelper::LibreOfficeKit::isActive() + ? 0 : rtl_getAppCommandArgCount()), + m_index(0) + { + OUString url; + if (utl::Bootstrap::getProcessWorkingDir(url)) { + m_cwdUrl = url; + } + } + + virtual std::optional< OUString > getCwdUrl() override { return m_cwdUrl; } + + virtual bool next(OUString * argument) override { + OSL_ASSERT(argument != nullptr); + if (m_index < m_count) { + rtl_getAppCommandArg(m_index++, &argument->pData); + return true; + } else { + return false; + } + } + +private: + std::optional< OUString > m_cwdUrl; + sal_uInt32 m_count; + sal_uInt32 m_index; +}; + +enum class CommandLineEvent { + Open, Print, View, Start, PrintTo, + ForceOpen, ForceNew, Conversion, BatchPrint +}; + +// Office URI Schemes: see https://msdn.microsoft.com/en-us/library/dn906146 +// This functions checks if the arg is an Office URI. +// If applicable, it updates arg to inner URI. +// If no event argument is explicitly set in command line, +// then it returns updated command line event, +// according to Office URI command. +CommandLineEvent CheckOfficeURI(/* in,out */ OUString& arg, CommandLineEvent curEvt) +{ + // 1. Strip the scheme name + OUString rest1; + bool isOfficeURI = ( arg.startsWithIgnoreAsciiCase("vnd.libreoffice.command:", &rest1) // Proposed extended schema + || arg.startsWithIgnoreAsciiCase("ms-word:", &rest1) + || arg.startsWithIgnoreAsciiCase("ms-powerpoint:", &rest1) + || arg.startsWithIgnoreAsciiCase("ms-excel:", &rest1) + || arg.startsWithIgnoreAsciiCase("ms-visio:", &rest1) + || arg.startsWithIgnoreAsciiCase("ms-access:", &rest1)); + if (!isOfficeURI) + return curEvt; + + OUString rest2; + tools::Long nURIlen = -1; + + // URL might be encoded + OUString decoded_rest = rest1.replaceAll("%7C", "|").replaceAll("%7c", "|"); + + // 2. Discriminate by command name (incl. 1st command argument descriptor) + // Extract URI: everything up to possible next argument + if (decoded_rest.startsWith("ofv|u|", &rest2)) + { + // Open for view - override only in default mode + if (curEvt == CommandLineEvent::Open) + curEvt = CommandLineEvent::View; + nURIlen = rest2.indexOf("|"); + } + else if (decoded_rest.startsWith("ofe|u|", &rest2)) + { + // Open for editing - override only in default mode + if (curEvt == CommandLineEvent::Open) + curEvt = CommandLineEvent::ForceOpen; + nURIlen = rest2.indexOf("|"); + } + else if (decoded_rest.startsWith("nft|u|", &rest2)) + { + // New from template - override only in default mode + if (curEvt == CommandLineEvent::Open) + curEvt = CommandLineEvent::ForceNew; + nURIlen = rest2.indexOf("|"); + // TODO: process optional second argument (default save-to location) + // For now, we just ignore it + } + else + { + // Abbreviated scheme: <scheme-name>:URI + // "ofv|u|" implied + // override only in default mode + if (curEvt == CommandLineEvent::Open) + curEvt = CommandLineEvent::View; + rest2 = rest1; + } + if (nURIlen < 0) + nURIlen = rest2.getLength(); + auto const uri = rest2.subView(0, nURIlen); + if (INetURLObject(uri).GetProtocol() == INetProtocol::Macro) { + // Let the "Open" machinery process the full command URI (leading to failure, by intention, + // as the "Open" machinery does not know about those command URI schemes): + curEvt = CommandLineEvent::Open; + } else { + arg = uri; + } + return curEvt; +} + +// Skip single newline (be it *NIX LF, MacOS CR, of Win CRLF) +// Changes the offset, and returns true if moved +bool SkipNewline(const char* & pStr) +{ + if ((*pStr != '\r') && (*pStr != '\n')) + return false; + if (*pStr == '\r') + ++pStr; + if (*pStr == '\n') + ++pStr; + return true; +} + +// Web query: http://support.microsoft.com/kb/157482 +CommandLineEvent CheckWebQuery(/* in,out */ OUString& arg, CommandLineEvent curEvt) +{ + // Only handle files with extension .iqy + if (!arg.endsWithIgnoreAsciiCase(".iqy")) + return curEvt; + + static osl::Mutex aMutex; + osl::MutexGuard aGuard(aMutex); + + try + { + OUString sFileURL; + // Cannot use translateExternalUris yet, because process service factory is not yet available + if (osl::FileBase::getFileURLFromSystemPath(arg, sFileURL) != osl::FileBase::RC::E_None) + return curEvt; + SvFileStream stream(sFileURL, StreamMode::READ); + + const sal_Int32 nBufLen = 32000; + char sBuffer[nBufLen]; + size_t nRead = stream.ReadBytes(sBuffer, nBufLen); + if (nRead < 8) // WEB\n1\n... + return curEvt; + + const char* pPos = sBuffer; + if (strncmp(pPos, "WEB", 3) != 0) + return curEvt; + pPos += 3; + if (!SkipNewline(pPos)) + return curEvt; + if (*pPos != '1') + return curEvt; + ++pPos; + if (!SkipNewline(pPos)) + return curEvt; + + OStringBuffer aResult(nRead); + do + { + const char* pPos1 = pPos; + const char* pEnd = sBuffer + nRead; + while ((pPos1 < pEnd) && (*pPos1 != '\r') && (*pPos1 != '\n')) + ++pPos1; + aResult.append(pPos, pPos1 - pPos); + if (pPos1 < pEnd) // newline + break; + pPos = sBuffer; + } while ((nRead = stream.ReadBytes(sBuffer, nBufLen)) > 0); + + stream.Close(); + + arg = OStringToOUString(aResult, osl_getThreadTextEncoding()); + return CommandLineEvent::ForceNew; + } + catch (...) + { + SAL_WARN("desktop.app", "An error processing Web Query file: " << arg); + } + + return curEvt; +} + +} // namespace + +CommandLineArgs::Supplier::Exception::Exception() {} + +CommandLineArgs::Supplier::Exception::Exception(Exception const &) {} + +CommandLineArgs::Supplier::Exception & +CommandLineArgs::Supplier::Exception::operator =(Exception const &) +{ return *this; } + +CommandLineArgs::Supplier::~Supplier() {} + +// initialize class with command line parameters from process environment +CommandLineArgs::CommandLineArgs() +{ + InitParamValues(); + ExtCommandLineSupplier s; + ParseCommandLine_Impl( s ); +} + +CommandLineArgs::CommandLineArgs( Supplier& supplier ) +{ + InitParamValues(); + ParseCommandLine_Impl( supplier ); +} + +void CommandLineArgs::ParseCommandLine_Impl( Supplier& supplier ) +{ + m_cwdUrl = supplier.getCwdUrl(); + CommandLineEvent eCurrentEvent = CommandLineEvent::Open; + + for (;;) + { + OUString aArg; + if ( !supplier.next( &aArg ) ) + { + break; + } + + if ( !aArg.isEmpty() ) + { + m_bEmpty = false; + OUString oArg; + OUString oDeprecatedArg; + if (!aArg.startsWith("--", &oArg) && aArg.startsWith("-", &oArg) + && aArg.getLength() > 2) // -h, -?, -n, -o, -p are still valid + { + oDeprecatedArg = aArg; // save here, since aArg can change later + } + + OUString rest; + if ( oArg == "minimized" ) + { + m_minimized = true; + } + else if ( oArg == "invisible" ) + { + m_invisible = true; + } + else if ( oArg == "norestore" ) + { + m_norestore = true; + } + else if ( oArg == "nodefault" ) + { + m_nodefault = true; + } + else if ( oArg == "headless" ) + { + setHeadless(); + } + else if ( oArg == "eventtesting" ) + { + m_eventtesting = true; + } + else if ( oArg == "safe-mode" ) + { + m_safemode = true; + } + else if ( oArg == "cat" ) + { + m_textcat = true; + m_conversionparams = "txt:Text"; + eCurrentEvent = CommandLineEvent::Conversion; + setHeadless(); + } + else if ( oArg == "script-cat" ) + { + m_scriptcat = true; + eCurrentEvent = CommandLineEvent::Conversion; + setHeadless(); + } + else if ( oArg == "quickstart" ) + { +#if defined(ENABLE_QUICKSTART_APPLET) + m_quickstart = true; +#endif + m_noquickstart = false; + } + else if ( oArg == "quickstart=no" ) + { + m_noquickstart = true; + m_quickstart = false; + } + else if ( oArg == "terminate_after_init" ) + { + m_terminateafterinit = true; + } + else if ( oArg == "nofirststartwizard" ) + { + // Do nothing, accept only for backward compatibility + } + else if ( oArg == "nologo" ) + { + m_nologo = true; + } +#if HAVE_FEATURE_MULTIUSER_ENVIRONMENT + else if ( oArg == "nolockcheck" ) + { + m_nolockcheck = true; + } +#endif + else if ( oArg == "help" || aArg == "-h" || aArg == "-?" ) + { + m_help = true; + } + else if ( oArg == "helpwriter" ) + { + m_helpwriter = true; + } + else if ( oArg == "helpcalc" ) + { + m_helpcalc = true; + } + else if ( oArg == "helpdraw" ) + { + m_helpdraw = true; + } + else if ( oArg == "helpimpress" ) + { + m_helpimpress = true; + } + else if ( oArg == "helpbase" ) + { + m_helpbase = true; + } + else if ( oArg == "helpbasic" ) + { + m_helpbasic = true; + } + else if ( oArg == "helpmath" ) + { + m_helpmath = true; + } + else if ( oArg == "protector" ) + { + // Not relevant for us here, but can be used in unit tests. + // Usually unit tests would not end up here, but e.g. the + // LOK Tiled Rendering tests end up running a full soffice + // process, and we can't bail on the use of --protector. + + // We specifically need to consume the following 2 arguments + // for --protector + if ((!supplier.next(&aArg) || !supplier.next(&aArg)) && m_unknown.isEmpty()) + m_unknown = "--protector must be followed by two arguments"; + } + else if ( oArg == "version" ) + { + m_version = true; + } + else if ( oArg.startsWith("splash-pipe=") ) + { + m_splashpipe = true; + } +#ifdef MACOSX + /* #i84053# ignore -psn on Mac + Platform dependent #ifdef here is ugly, however this is currently + the only platform dependent parameter. Should more appear + we should find a better solution + */ + else if ( aArg.startsWith("-psn") ) + { + oDeprecatedArg.clear(); + } +#endif +#if HAVE_FEATURE_MACOSX_SANDBOX + else if ( oArg == "nstemporarydirectory" ) + { + printf("%s\n", [NSTemporaryDirectory() UTF8String]); + exit(0); + } +#endif +#ifdef _WIN32 + /* fdo#57203 ignore -Embedding on Windows + when LibreOffice is launched by COM+ + */ + else if ( oArg == "Embedding" ) + { + oDeprecatedArg.clear(); + } +#endif + else if ( oArg.startsWith("infilter=", &rest)) + { + m_infilter.push_back(rest); + } + else if ( oArg.startsWith("accept=", &rest)) + { + m_accept.push_back(rest); + } + else if ( oArg.startsWith("unaccept=", &rest)) + { + m_unaccept.push_back(rest); + } + else if ( oArg.startsWith("language=", &rest)) + { + m_language = rest; + } + else if ( oArg.startsWith("pidfile=", &rest)) + { + m_pidfile = rest; + } + else if ( oArg == "writer" ) + { + m_writer = true; + m_bDocumentArgs = true; + } + else if ( oArg == "calc" ) + { + m_calc = true; + m_bDocumentArgs = true; + } + else if ( oArg == "draw" ) + { + m_draw = true; + m_bDocumentArgs = true; + } + else if ( oArg == "impress" ) + { + m_impress = true; + m_bDocumentArgs = true; + } + else if ( oArg == "base" ) + { + m_base = true; + m_bDocumentArgs = true; + } + else if ( oArg == "global" ) + { + m_global = true; + m_bDocumentArgs = true; + } + else if ( oArg == "math" ) + { + m_math = true; + m_bDocumentArgs = true; + } + else if ( oArg == "web" ) + { + m_web = true; + m_bDocumentArgs = true; + } + else if ( aArg == "-n" ) + { + // force new documents based on the following documents + eCurrentEvent = CommandLineEvent::ForceNew; + } + else if ( aArg == "-o" ) + { + // force open documents regardless if they are templates or not + eCurrentEvent = CommandLineEvent::ForceOpen; + } + else if ( oArg == "pt" ) + { + // Print to special printer + eCurrentEvent = CommandLineEvent::PrintTo; + // first argument after "-pt" must be the printer name + if (supplier.next(&aArg)) + m_printername = aArg; + else if (m_unknown.isEmpty()) + m_unknown = "--pt must be followed by printername"; + } + else if ( aArg == "-p" ) + { + // Print to default printer + eCurrentEvent = CommandLineEvent::Print; + } + else if ( oArg == "view") + { + // open in viewmode + eCurrentEvent = CommandLineEvent::View; + } + else if ( oArg == "show" ) + { + // open in viewmode + eCurrentEvent = CommandLineEvent::Start; + } + else if ( oArg == "display" ) + { + // The command line argument following --display should + // always be treated as the argument of --display. + // --display and its argument are handled "out of line" + // in Unix-only desktop/unx/source/splashx.c and vcl/unx/*, + // and just ignored here + (void)supplier.next(&aArg); + } + else if ( oArg == "convert-to" ) + { + eCurrentEvent = CommandLineEvent::Conversion; + // first argument must be the params + if (supplier.next(&aArg)) + { + m_conversionparams = aArg; + // It doesn't make sense to use convert-to without headless. + setHeadless(); + } + else if (m_unknown.isEmpty()) + m_unknown = "--convert-to must be followed by output_file_extension[:output_filter_name]"; + } + else if ( oArg == "print-to-file" ) + { + eCurrentEvent = CommandLineEvent::BatchPrint; + } + else if ( oArg == "printer-name" ) + { + if (eCurrentEvent == CommandLineEvent::BatchPrint) + { + // first argument is the printer name + if (supplier.next(&aArg)) + m_printername = aArg; + else if (m_unknown.isEmpty()) + m_unknown = "--printer-name must be followed by printername"; + } + else if (m_unknown.isEmpty()) + { + m_unknown = "--printer-name must directly follow --print-to-file"; + } + } + else if ( oArg == "outdir" ) + { + if (eCurrentEvent == CommandLineEvent::Conversion || + eCurrentEvent == CommandLineEvent::BatchPrint) + { + if (supplier.next(&aArg)) + m_conversionout = aArg; + else if (m_unknown.isEmpty()) + m_unknown = "--outdir must be followed by output directory path"; + } + else if (m_unknown.isEmpty()) + { + m_unknown = "--outdir must directly follow either output filter specification of --convert-to, or --print-to-file or its printer specification"; + } + } + else if ( eCurrentEvent == CommandLineEvent::Conversion + && oArg == "convert-images-to" ) + { + if (supplier.next(&aArg)) + m_convertimages = aArg; + else if (m_unknown.isEmpty()) + m_unknown = "--convert-images-to must be followed by an image type"; + } + else if ( aArg.startsWith("-") ) + { + // because it's impossible to filter these options that + // are handled in the soffice shell script with the + // primitive tools that /bin/sh offers, ignore them here + if ( +#if defined UNX + oArg != "record" && + oArg != "backtrace" && + oArg != "strace" && + oArg != "valgrind" && + // for X Session Management, handled in + // vcl/unx/generic/app/sm.cxx: + oArg != "session=" && +#endif + //ignore additional legacy options that don't do anything anymore + oArg != "nocrashreport" && + m_unknown.isEmpty()) + { + m_unknown = aArg; + } + oDeprecatedArg.clear(); + } + else + { + // handle this argument as a filename + + // First check if this is an Office URI + // This will possibly adjust event for this argument + // and put real URI to aArg + CommandLineEvent eThisEvent = CheckOfficeURI(aArg, eCurrentEvent); + + // Now check if this is a Web Query file + eThisEvent = CheckWebQuery(aArg, eThisEvent); + + switch (eThisEvent) + { + case CommandLineEvent::Open: + m_openlist.push_back(aArg); + m_bDocumentArgs = true; + break; + case CommandLineEvent::View: + m_viewlist.push_back(aArg); + m_bDocumentArgs = true; + break; + case CommandLineEvent::Start: + m_startlist.push_back(aArg); + m_bDocumentArgs = true; + break; + case CommandLineEvent::Print: + m_printlist.push_back(aArg); + m_bDocumentArgs = true; + break; + case CommandLineEvent::PrintTo: + m_printtolist.push_back(aArg); + m_bDocumentArgs = true; + break; + case CommandLineEvent::ForceNew: + m_forcenewlist.push_back(aArg); + m_bDocumentArgs = true; + break; + case CommandLineEvent::ForceOpen: + m_forceopenlist.push_back(aArg); + m_bDocumentArgs = true; + break; + case CommandLineEvent::Conversion: + case CommandLineEvent::BatchPrint: + m_conversionlist.push_back(aArg); + break; + } + } + + if (!oDeprecatedArg.isEmpty()) + { + OString sArg(OUStringToOString(oDeprecatedArg, osl_getThreadTextEncoding())); + fprintf(stderr, "Warning: %s is deprecated. Use -%s instead.\n", sArg.getStr(), sArg.getStr()); + } + } + } +} + +void CommandLineArgs::InitParamValues() +{ + m_minimized = false; + m_norestore = false; +#if HAVE_FEATURE_UI + m_invisible = false; + m_headless = false; +#else + m_invisible = true; + m_headless = true; +#endif + m_eventtesting = false; + m_quickstart = false; + m_noquickstart = false; + m_terminateafterinit = false; + m_nologo = false; + m_nolockcheck = false; + m_nodefault = false; + m_help = false; + m_writer = false; + m_calc = false; + m_draw = false; + m_impress = false; + m_global = false; + m_math = false; + m_web = false; + m_base = false; + m_helpwriter = false; + m_helpcalc = false; + m_helpdraw = false; + m_helpbasic = false; + m_helpmath = false; + m_helpimpress = false; + m_helpbase = false; + m_version = false; + m_splashpipe = false; + m_bEmpty = true; + m_bDocumentArgs = false; + m_textcat = false; + m_scriptcat = false; + m_safemode = false; +} + +bool CommandLineArgs::HasModuleParam() const +{ + return m_writer || m_calc || m_draw || m_impress || m_global || m_math + || m_web || m_base; +} + +std::vector< OUString > CommandLineArgs::GetOpenList() const +{ + return translateExternalUris(m_openlist); +} + +std::vector< OUString > CommandLineArgs::GetViewList() const +{ + return translateExternalUris(m_viewlist); +} + +std::vector< OUString > CommandLineArgs::GetStartList() const +{ + return translateExternalUris(m_startlist); +} + +std::vector< OUString > CommandLineArgs::GetForceOpenList() const +{ + return translateExternalUris(m_forceopenlist); +} + +std::vector< OUString > CommandLineArgs::GetForceNewList() const +{ + return translateExternalUris(m_forcenewlist); +} + +std::vector< OUString > CommandLineArgs::GetPrintList() const +{ + return translateExternalUris(m_printlist); +} + +std::vector< OUString > CommandLineArgs::GetPrintToList() const +{ + return translateExternalUris(m_printtolist); +} + +std::vector< OUString > CommandLineArgs::GetConversionList() const +{ + return translateExternalUris(m_conversionlist); +} + +OUString CommandLineArgs::GetConversionOut() const +{ + return translateExternalUris(m_conversionout); +} + +} // namespace desktop + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/cmdlineargs.hxx b/desktop/source/app/cmdlineargs.hxx new file mode 100644 index 000000000..74236f5b3 --- /dev/null +++ b/desktop/source/app/cmdlineargs.hxx @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <sal/config.h> + +#include <vector> + +#include <rtl/ustring.hxx> +#include <optional> + +namespace desktop +{ + +class CommandLineArgs +{ + public: + struct Supplier + { + // Thrown from constructors and next: + class Exception final + { + public: + Exception(); + Exception(Exception const &); + Exception & operator =(Exception const &); + }; + + virtual ~Supplier(); + virtual std::optional< OUString > getCwdUrl() = 0; + virtual bool next(OUString * argument) = 0; + }; + + CommandLineArgs(); + explicit CommandLineArgs( Supplier& supplier ); + + CommandLineArgs(const CommandLineArgs&) = delete; + const CommandLineArgs& operator=(const CommandLineArgs&) = delete; + + const std::optional< OUString >& getCwdUrl() const { return m_cwdUrl; } + + // Access to bool parameters + bool IsMinimized() const { return m_minimized;} + bool IsInvisible() const + { + return m_invisible || (m_headless && !m_eventtesting); + } + bool IsNoRestore() const { return m_norestore;} + bool IsNoDefault() const { return m_nodefault;} + bool IsHeadless() const { return m_headless;} + bool IsEventTesting() const { return m_eventtesting;} + bool IsQuickstart() const { return m_quickstart;} + bool IsNoQuickstart() const { return m_noquickstart;} + bool IsTerminateAfterInit() const { return m_terminateafterinit;} + bool IsNoLogo() const { return m_nologo;} + bool IsNoLockcheck() const { return m_nolockcheck;} + bool IsHelp() const { return m_help;} + bool IsHelpWriter() const { return m_helpwriter;} + bool IsHelpCalc() const { return m_helpcalc;} + bool IsHelpDraw() const { return m_helpdraw;} + bool IsHelpImpress() const { return m_helpimpress;} + bool IsHelpBase() const { return m_helpbase;} + bool IsHelpMath() const { return m_helpmath;} + bool IsHelpBasic() const { return m_helpbasic;} + bool IsWriter() const { return m_writer;} + bool IsCalc() const { return m_calc;} + bool IsDraw() const { return m_draw;} + bool IsImpress() const { return m_impress;} + bool IsBase() const { return m_base;} + bool IsGlobal() const { return m_global;} + bool IsMath() const { return m_math;} + bool IsWeb() const { return m_web;} + bool IsVersion() const { return m_version;} + bool HasModuleParam() const; + bool WantsToLoadDocument() const { return m_bDocumentArgs;} + bool IsTextCat() const { return m_textcat;} + bool IsScriptCat() const { return m_scriptcat;} + bool IsSafeMode() const { return m_safemode; } + + const OUString& GetUnknown() const { return m_unknown;} + + // Access to string parameters + bool HasSplashPipe() const { return m_splashpipe;} + std::vector< OUString > const & GetAccept() const { return m_accept;} + std::vector< OUString > const & GetUnaccept() const { return m_unaccept;} + std::vector< OUString > GetOpenList() const; + std::vector< OUString > GetViewList() const; + std::vector< OUString > GetStartList() const; + std::vector< OUString > GetForceOpenList() const; + std::vector< OUString > GetForceNewList() const; + std::vector< OUString > GetPrintList() const; + std::vector< OUString > GetPrintToList() const; + const OUString& GetPrinterName() const { return m_printername;} + const OUString& GetLanguage() const { return m_language;} + std::vector< OUString > const & GetInFilter() const { return m_infilter;} + std::vector< OUString > GetConversionList() const; + const OUString& GetConversionParams() const { return m_conversionparams;} + OUString GetConversionOut() const; + OUString const & GetImageConversionType() const { return m_convertimages; } + const OUString& GetPidfileName() const { return m_pidfile;} + + // Special analyzed states (does not match directly to a command line parameter!) + bool IsEmpty() const { return m_bEmpty;} + + void setHeadless() { m_headless = true; } + + private: + void ParseCommandLine_Impl( Supplier& supplier ); + void InitParamValues(); + + std::optional< OUString > m_cwdUrl; + + bool m_minimized; + bool m_invisible; + bool m_norestore; + bool m_headless; + bool m_eventtesting; + bool m_quickstart; + bool m_noquickstart; + bool m_terminateafterinit; + bool m_nologo; + bool m_nolockcheck; + bool m_nodefault; + bool m_help; + bool m_writer; + bool m_calc; + bool m_draw; + bool m_impress; + bool m_global; + bool m_math; + bool m_web; + bool m_base; + bool m_helpwriter; + bool m_helpcalc; + bool m_helpdraw; + bool m_helpbasic; + bool m_helpmath; + bool m_helpimpress; + bool m_helpbase; + bool m_version; + bool m_splashpipe; + bool m_textcat; + bool m_scriptcat; + bool m_safemode; + + OUString m_unknown; + + bool m_bEmpty; // No Args at all + bool m_bDocumentArgs; // A document creation/open/load arg is used + std::vector< OUString > m_accept; + std::vector< OUString > m_unaccept; + std::vector< OUString > m_openlist; // contains external URIs + std::vector< OUString > m_viewlist; // contains external URIs + std::vector< OUString > m_startlist; // contains external URIs + std::vector< OUString > m_forceopenlist; // contains external URIs + std::vector< OUString > m_forcenewlist; // contains external URIs + std::vector< OUString > m_printlist; // contains external URIs + std::vector< OUString > m_printtolist; // contains external URIs + OUString m_printername; + std::vector< OUString > m_conversionlist; // contains external URIs + OUString m_conversionparams; + OUString m_conversionout; // contains external URIs + OUString m_convertimages; // The format in which images should be converted + std::vector< OUString > m_infilter; + OUString m_language; + OUString m_pidfile; +}; + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/cmdlinehelp.cxx b/desktop/source/app/cmdlinehelp.cxx new file mode 100644 index 000000000..419b03b3a --- /dev/null +++ b/desktop/source/app/cmdlinehelp.cxx @@ -0,0 +1,263 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <stdio.h> +#include <comphelper/string.hxx> +#include <app.hxx> + +#include "cmdlinehelp.hxx" + +#ifdef _WIN32 +#if !defined WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +#endif +#include <windows.h> +#include <io.h> +#include <fcntl.h> +#endif + +namespace desktop +{ + constexpr OUStringLiteral aCmdLineHelp_version = + u"%PRODUCTNAME %PRODUCTVERSION%PRODUCTEXTENSION %BUILDID\n" + "\n"; + constexpr OUStringLiteral aCmdLineHelp = + u"Usage: %CMDNAME [argument...]\n" + " argument - switches, switch parameters and document URIs (filenames). \n\n" + "Using without special arguments: \n" + "Opens the start center, if it is used without any arguments. \n" + " {file} Tries to open the file (files) in the components \n" + " suitable for them. \n" + " {file} {macro:///Library.Module.MacroName} \n" + " Opens the file and runs specified macros from \n" + " the file. \n\n" + "Getting help and information: \n" + " --help | -h | -? Shows this help and quits. \n" + " --helpwriter Opens built-in or online Help on Writer. \n" + " --helpcalc Opens built-in or online Help on Calc. \n" + " --helpdraw Opens built-in or online Help on Draw. \n" + " --helpimpress Opens built-in or online Help on Impress. \n" + " --helpbase Opens built-in or online Help on Base. \n" + " --helpbasic Opens built-in or online Help on Basic scripting \n" + " language. \n" + " --helpmath Opens built-in or online Help on Math. \n" + " --version Shows the version and quits. \n" + " --nstemporarydirectory \n" + " (MacOS X sandbox only) Returns path of the temporary \n" + " directory for the current user and exits. Overrides \n" + " all other arguments. \n\n" + "General arguments: \n" + " --quickstart[=no] Activates[Deactivates] the Quickstarter service. \n" + " --nolockcheck Disables check for remote instances using one \n" + " installation. \n" + " --infilter={filter} Force an input filter type if possible. For example: \n" + " --infilter=\"Calc Office Open XML\" \n" + " --infilter=\"Text (encoded):UTF8,LF,,,\" \n" + " --pidfile={file} Store soffice.bin pid to {file}. \n" + " --display {display} Sets the DISPLAY environment variable on UNIX-like \n" + " platforms to the value {display} (only supported by a \n" + " start script). \n\n" + "User/programmatic interface control: \n" + " --nologo Disables the splash screen at program start. \n" + " --minimized Starts minimized. The splash screen is not displayed. \n" + " --nodefault Starts without displaying anything except the splash \n" + " screen (do not display initial window). \n" + " --invisible Starts in invisible mode. Neither the start-up logo nor \n" + " the initial program window will be visible. Application \n" + " can be controlled, and documents and dialogs can be \n" + " controlled and opened via the API. Using the parameter, \n" + " the process can only be ended using the taskmanager \n" + " (Windows) or the kill command (UNIX-like systems). It \n" + " cannot be used in conjunction with --quickstart. \n" + " --headless Starts in \"headless mode\" which allows using the \n" + " application without GUI. This special mode can be used \n" + " when the application is controlled by external clients \n" + " via the API. \n" + " --norestore Disables restart and file recovery after a system crash.\n" + " --safe-mode Starts in a safe mode, i.e. starts temporarily with a \n" + " fresh user profile and helps to restore a broken \n" + " configuration. \n" + " --accept={connect-string} Specifies a UNO connect-string to create a UNO \n" + " acceptor through which other programs can connect to \n" + " access the API. Note that API access allows execution \n" + " of arbitrary commands. \n" + " The syntax of the {connect-string} is: \n" + " connection-type,params;protocol-name,params \n" + " e.g. pipe,name={some name};urp \n" + " or socket,host=localhost,port=54321;urp \n" + " --unaccept={connect-string} Closes an acceptor that was created with \n" + " --accept. Use --unaccept=all to close all acceptors. \n" + " --language={lang} Uses specified language, if language is not selected \n" + " yet for UI. The lang is a tag of the language in IETF \n" + " language tag. \n\n" + "Developer arguments: \n" + " --terminate_after_init \n" + " Exit after initialization complete (no documents loaded)\n" + " --eventtesting Exit after loading documents. \n\n" + "New document creation arguments: \n" + "The arguments create an empty document of specified kind. Only one of them may \n" + "be used in one command line. If filenames are specified after an argument, \n" + "then it tries to open those files in the specified component. \n" + " --writer Creates an empty Writer document. \n" + " --calc Creates an empty Calc document. \n" + " --draw Creates an empty Draw document. \n" + " --impress Creates an empty Impress document. \n" + " --base Creates a new database. \n" + " --global Creates an empty Writer master (global) document. \n" + " --math Creates an empty Math document (formula). \n" + " --web Creates an empty HTML document. \n\n" + "File open arguments: \n" + "The arguments define how following filenames are treated. New treatment begins \n" + "after the argument and ends at the next argument. The default treatment is to \n" + "open documents for editing, and create new documents from document templates. \n" + " -n Treats following files as templates for creation of new \n" + " documents. \n" + " -o Opens following files for editing, regardless whether \n" + " they are templates or not. \n" + " --pt {Printername} Prints following files to the printer {Printername}, \n" + " after which those files are closed. The splash screen \n" + " does not appear. If used multiple times, only last \n" + " {Printername} is effective for all documents of all \n" + " --pt runs. Also, --printer-name argument of \n" + " --print-to-file switch interferes with {Printername}. \n" + " -p Prints following files to the default printer, after \n" + " which those files are closed. The splash screen does \n" + " not appear. If the file name contains spaces, then it \n" + " must be enclosed in quotation marks. \n" + " --view Opens following files in viewer mode (read-only). \n" + " --show Opens and starts the following presentation documents \n" + " of each immediately. Files are closed after the showing.\n" + " Files other than Impress documents are opened in \n" + " default mode , regardless of previous mode. \n" + " --convert-to OutputFileExtension[:OutputFilterName] \\ \n" + " [--outdir output_dir] [--convert-images-to] \n" + " Batch convert files (implies --headless). If --outdir \n" + " isn't specified, then current working directory is used \n" + " as output_dir. If --convert-images-to is given, its \n" + " parameter is taken as the target filter format for *all*\n" + " images written to the output format. If --convert-to is \n" + " used more than once, the last value of \n" + " OutputFileExtension[:OutputFilterName] is effective. If \n" + " --outdir is used more than once, only its last value is \n" + " effective. For example: \n" + " --convert-to pdf *.odt \n" + " --convert-to epub *.doc \n" + " --convert-to pdf:writer_pdf_Export --outdir /home/user *.doc\n" + " --convert-to \"html:XHTML Writer File:UTF8\" \\ \n" + " --convert-images-to \"jpg\" *.doc \n" + " --convert-to \"txt:Text (encoded):UTF8\" *.doc \n" + " --print-to-file [--printer-name printer_name] [--outdir output_dir] \n" + " Batch print files to file. If --outdir is not specified,\n" + " then current working directory is used as output_dir. \n" + " If --printer-name or --outdir used multiple times, only \n" + " last value of each is effective. Also, {Printername} of \n" + " --pt switch interferes with --printer-name. \n" + " --cat Dump text content of the following files to console \n" + " (implies --headless). Cannot be used with --convert-to. \n" + " --script-cat Dump text content of any scripts embedded in the files \n" + " to console (implies --headless). Cannot be used with \n" + " --convert-to. \n" + " -env:<VAR>[=<VALUE>] Set a bootstrap variable. For example: to set \n" + " a non-default user profile path: \n" + " -env:UserInstallation=file:///tmp/test \n\n" + "Ignored switches: \n" + " -psn Ignored (MacOS X only). \n" + " -Embedding Ignored (COM+ related; Windows only). \n" + " --nofirststartwizard Does nothing, accepted only for backward compatibility.\n" + " --protector {arg1} {arg2} \n" + " Used only in unit tests and should have two arguments. \n\n"; +#ifdef _WIN32 + namespace{ + // This class is only used to create a console when soffice.bin is run without own console + // (like using soffice.exe launcher as opposed to soffice.com), and either --version or + // --help command line options were specified, or an error in a command line option was + // detected, which requires to output strings to user. + class lcl_Console { + public: + explicit lcl_Console(short nBufHeight) + : m_bOwnConsole(AllocConsole() != FALSE) + { + if (m_bOwnConsole) + { + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + + // Ensure that console buffer is enough to hold required data + CONSOLE_SCREEN_BUFFER_INFO cinfo; + GetConsoleScreenBufferInfo(hOut, &cinfo); + if (cinfo.dwSize.Y < nBufHeight) + { + cinfo.dwSize.Y = nBufHeight; + SetConsoleScreenBufferSize(hOut, cinfo.dwSize); + } + + (void)freopen("CON", "r", stdin); + (void)freopen("CON", "w", stdout); + (void)freopen("CON", "w", stderr); + + std::ios::sync_with_stdio(true); + } + } + + ~lcl_Console() + { + if (m_bOwnConsole) + { + fflush(stdout); + fprintf(stdout, "Press Enter to continue..."); + fgetc(stdin); + FreeConsole(); + } + } + private: + bool m_bOwnConsole; + }; + } +#endif + + void displayCmdlineHelp(OUString const & unknown) + { + OUString aHelpMessage_version = ReplaceStringHookProc(aCmdLineHelp_version); + OUString aHelpMessage(OUString(aCmdLineHelp).replaceFirst("%CMDNAME", "soffice")); + if (!unknown.isEmpty()) + { + aHelpMessage = "Error in option: " + unknown + "\n\n" + + aHelpMessage; + } +#ifdef _WIN32 + sal_Int32 n = comphelper::string::getTokenCount(aHelpMessage, '\n'); + lcl_Console aConsole(short(n*2)); +#endif + fprintf(stdout, "%s%s", + OUStringToOString(aHelpMessage_version, RTL_TEXTENCODING_ASCII_US).getStr(), + OUStringToOString(aHelpMessage, RTL_TEXTENCODING_ASCII_US).getStr()); + } + + void displayVersion() + { + OUString aVersionMsg(aCmdLineHelp_version); + aVersionMsg = ReplaceStringHookProc(aVersionMsg); +#ifdef _WIN32 + lcl_Console aConsole(short(10)); +#endif + fprintf(stdout, "%s", OUStringToOString(aVersionMsg, RTL_TEXTENCODING_ASCII_US).getStr()); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/cmdlinehelp.hxx b/desktop/source/app/cmdlinehelp.hxx new file mode 100644 index 000000000..5a3212505 --- /dev/null +++ b/desktop/source/app/cmdlinehelp.hxx @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <rtl/ustring.hxx> + +namespace desktop +{ +void displayCmdlineHelp(OUString const& unknown); +void displayVersion(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/crashreport.cxx b/desktop/source/app/crashreport.cxx new file mode 100644 index 000000000..0458edf97 --- /dev/null +++ b/desktop/source/app/crashreport.cxx @@ -0,0 +1,478 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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 <desktop/crashreport.hxx> +#include <rtl/bootstrap.hxx> +#include <osl/file.hxx> +#include <comphelper/processfactory.hxx> +#include <ucbhelper/proxydecider.hxx> +#include <unotools/bootstrap.hxx> +#include <o3tl/char16_t2wchar_t.hxx> +#include <desktop/minidump.hxx> +#include <rtl/ustrbuf.hxx> + +#include <config_version.h> +#include <config_folders.h> + +#include <string> +#include <regex> + + +#if HAVE_FEATURE_BREAKPAD + +#include <fstream> +#if defined( UNX ) && !defined MACOSX && !defined IOS && !defined ANDROID +#include <client/linux/handler/exception_handler.h> +#elif defined _WIN32 +#if defined __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmicrosoft-enum-value" +#endif +#include <client/windows/handler/exception_handler.h> +#if defined __clang__ +#pragma clang diagnostic pop +#endif +#include <locale> +#include <codecvt> +#endif + +osl::Mutex CrashReporter::maMutex; +osl::Mutex CrashReporter::maActiveSfxObjectNameMutex; +osl::Mutex CrashReporter::maUnoLogCmdMutex; +std::unique_ptr<google_breakpad::ExceptionHandler> CrashReporter::mpExceptionHandler; +bool CrashReporter::mbInit = false; +CrashReporter::vmaKeyValues CrashReporter::maKeyValues; +CrashReporter::vmaloggedUnoCommands CrashReporter::maloggedUnoCommands; +OUString CrashReporter::msActiveSfxObjectName; + + +#if defined( UNX ) && !defined MACOSX && !defined IOS && !defined ANDROID +static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* /*context*/, bool succeeded) +{ + CrashReporter::addKeyValue("Active-SfxObject",CrashReporter::getActiveSfxObjectName(),CrashReporter::AddItem); + CrashReporter::addKeyValue("Last-4-Uno-Commands",CrashReporter::getLoggedUnoCommands(),CrashReporter::AddItem); + CrashReporter::addKeyValue("DumpFile", OStringToOUString(descriptor.path(), RTL_TEXTENCODING_UTF8), CrashReporter::Write); + SAL_WARN("desktop", "minidump generated: " << descriptor.path()); + + return succeeded; +} +#elif defined _WIN32 +static bool dumpCallback(const wchar_t* path, const wchar_t* id, + void* /*context*/, EXCEPTION_POINTERS* /*exinfo*/, + MDRawAssertionInfo* /*assertion*/, + bool succeeded) +{ + // TODO: moggi: can we avoid this conversion +#ifdef _MSC_VER +#pragma warning (disable: 4996) +#endif + std::wstring_convert<std::codecvt_utf8<wchar_t>> conv1; + std::string aPath = conv1.to_bytes(std::wstring(path)) + conv1.to_bytes(std::wstring(id)) + ".dmp"; + CrashReporter::addKeyValue("Active-SfxObject",CrashReporter::getActiveSfxObjectName(),CrashReporter::AddItem); + CrashReporter::addKeyValue("Last-4-Uno-Commands",CrashReporter::getLoggedUnoCommands(),CrashReporter::AddItem); + CrashReporter::addKeyValue("DumpFile", OStringToOUString(aPath.c_str(), RTL_TEXTENCODING_UTF8), CrashReporter::AddItem); + CrashReporter::addKeyValue("GDIHandles", OUString::number(::GetGuiResources(::GetCurrentProcess(), GR_GDIOBJECTS)), CrashReporter::Write); + SAL_WARN("desktop", "minidump generated: " << aPath); + return succeeded; +} +#endif + + +void CrashReporter::writeToFile(std::ios_base::openmode Openmode) +{ +#if defined _WIN32 + const std::string iniPath = getIniFileName(); + std::wstring iniPathW; + const int nChars = MultiByteToWideChar(CP_UTF8, 0, iniPath.c_str(), -1, nullptr, 0); + auto buf = std::make_unique<wchar_t[]>(nChars); + if (MultiByteToWideChar(CP_UTF8, 0, iniPath.c_str(), -1, buf.get(), nChars) != 0) + iniPathW = buf.get(); + + std::ofstream ini_file + = iniPathW.empty() ? std::ofstream(iniPath, Openmode) : std::ofstream(iniPathW, Openmode); +#else + std::ofstream ini_file(getIniFileName(), Openmode); +#endif + + for (auto& keyValue : maKeyValues) + { + ini_file << OUStringToOString(keyValue.first, RTL_TEXTENCODING_UTF8) << "="; + ini_file << OUStringToOString(keyValue.second, RTL_TEXTENCODING_UTF8) << "\n"; + } + + maKeyValues.clear(); + ini_file.close(); +} + +void CrashReporter::addKeyValue(const OUString& rKey, const OUString& rValue, tAddKeyHandling AddKeyHandling) +{ + osl::MutexGuard aGuard(maMutex); + + if (IsDumpEnable()) + { + if (!rKey.isEmpty()) + maKeyValues.push_back(mpair(rKey, rValue)); + + if (AddKeyHandling != AddItem) + { + if (mbInit) + writeToFile(std::ios_base::app); + else if (AddKeyHandling == Create) + writeCommonInfo(); + } + } +} + +void CrashReporter::writeCommonInfo() +{ + writeSystemInfo(); + + ucbhelper::InternetProxyDecider proxy_decider(::comphelper::getProcessComponentContext()); + + static const OUStringLiteral protocol = u"https"; + static const OUStringLiteral url = u"crashreport.libreoffice.org"; + const sal_Int32 port = 443; + + const ucbhelper::InternetProxyServer proxy_server = proxy_decider.getProxy(protocol, url, port); + + // save the new Keys + vmaKeyValues atlast = maKeyValues; + // clear the keys, the following Keys should be at the begin + maKeyValues.clear(); + + // limit the amount of code that needs to be executed before the crash reporting + addKeyValue("ProductName", "LibreOffice", AddItem); + addKeyValue("Version", LIBO_VERSION_DOTTED, AddItem); + addKeyValue("BuildID", utl::Bootstrap::getBuildIdData(""), AddItem); + addKeyValue("URL", protocol + "://" + url + "/submit/", AddItem); + + if (!proxy_server.aName.isEmpty()) + { + addKeyValue("Proxy", proxy_server.aName + ":" + OUString::number(proxy_server.nPort), AddItem); + } + + // write the new keys at the end + maKeyValues.insert(maKeyValues.end(), atlast.begin(), atlast.end()); + + mbInit = true; + + writeToFile(std::ios_base::trunc); + + updateMinidumpLocation(); +} + +void CrashReporter::setActiveSfxObjectName(const OUString& rActiveSfxObjectName) +{ + osl::MutexGuard aGuard(maActiveSfxObjectNameMutex); + msActiveSfxObjectName = rActiveSfxObjectName; +} + +OUString CrashReporter::getActiveSfxObjectName() +{ + osl::MutexGuard aGuard(maActiveSfxObjectNameMutex); + return msActiveSfxObjectName; +} + +void CrashReporter::logUnoCommand(const OUString& rUnoCommand) +{ + osl::MutexGuard aGuard(maUnoLogCmdMutex); + + if( maloggedUnoCommands.size() == 4 ) + maloggedUnoCommands.pop_front(); + + maloggedUnoCommands.push_back(rUnoCommand); +} + +OUString CrashReporter::getLoggedUnoCommands() +{ + osl::MutexGuard aGuard(maUnoLogCmdMutex); + + OUString aCommandSeperator=""; + OUStringBuffer aUnoCommandBuffer; + + for( auto& unocommand: maloggedUnoCommands) + { + aUnoCommandBuffer.append(aCommandSeperator); + aUnoCommandBuffer.append(unocommand); + aCommandSeperator=","; + } + return aUnoCommandBuffer.makeStringAndClear(); +} + +namespace { + +OUString getCrashDirectory() +{ + OUString aCrashURL; + rtl::Bootstrap::get("CrashDirectory", aCrashURL); + // Need to convert to URL in case of user-defined path + osl::FileBase::getFileURLFromSystemPath(aCrashURL, aCrashURL); + + if (aCrashURL.isEmpty()) { // Fall back to user profile + aCrashURL = "${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/crash/"; + rtl::Bootstrap::expandMacros(aCrashURL); + } + + if (!aCrashURL.endsWith("/")) + aCrashURL += "/"; + + osl::Directory::create(aCrashURL); + OUString aCrashPath; + osl::FileBase::getSystemPathFromFileURL(aCrashURL, aCrashPath); + return aCrashPath; +} + +} + +void CrashReporter::updateMinidumpLocation() +{ +#if defined( UNX ) && !defined MACOSX && !defined IOS && !defined ANDROID + OUString aURL = getCrashDirectory(); + OString aOStringUrl = OUStringToOString(aURL, RTL_TEXTENCODING_UTF8); + google_breakpad::MinidumpDescriptor descriptor(aOStringUrl.getStr()); + mpExceptionHandler->set_minidump_descriptor(descriptor); +#elif defined _WIN32 + OUString aURL = getCrashDirectory(); + mpExceptionHandler->set_dump_path(o3tl::toW(aURL.getStr())); +#endif +} + +bool CrashReporter::crashReportInfoExists() +{ + static const bool InfoExist = crashreport::readConfig(CrashReporter::getIniFileName(), nullptr); + return InfoExist; +} + +bool CrashReporter::readSendConfig(std::string& response) +{ + return crashreport::readConfig(CrashReporter::getIniFileName(), &response); +} + +void CrashReporter::installExceptionHandler() +{ + if (!IsDumpEnable()) + return; +#if defined( UNX ) && !defined MACOSX && !defined IOS && !defined ANDROID + google_breakpad::MinidumpDescriptor descriptor("/tmp"); + mpExceptionHandler = std::make_unique<google_breakpad::ExceptionHandler>(descriptor, nullptr, dumpCallback, nullptr, true, -1); +#elif defined _WIN32 + mpExceptionHandler = std::make_unique<google_breakpad::ExceptionHandler>(L".", nullptr, dumpCallback, nullptr, google_breakpad::ExceptionHandler::HANDLER_ALL); +#endif +} + +void CrashReporter::removeExceptionHandler() +{ + mpExceptionHandler.reset(); +} + + + +bool CrashReporter::IsDumpEnable() +{ + auto const env = std::getenv("CRASH_DUMP_ENABLE"); + if (env != nullptr && env[0] != '\0') { + return true; + } + // read configuration item 'CrashDumpEnable' -> bool on/off + OUString sToken; + if (rtl::Bootstrap::get("CrashDumpEnable", sToken)) + { + return sToken.toBoolean(); + } + return true; // default, always on +} + + +std::string CrashReporter::getIniFileName() +{ + OUString url = getCrashDirectory() + "dump.ini"; + OString aUrl = OUStringToOString(url, RTL_TEXTENCODING_UTF8); + std::string aRet(aUrl.getStr()); + return aRet; +} + +// Write system-specific information such as the CPU name and features. +// This may allow us to get some statistics for decisions (such as when +// deciding whether SSE2 can be made a hard-requirement for Windows). +// Breakpad provides this information poorly or not at all. +#if defined( UNX ) && !defined MACOSX && !defined IOS && !defined ANDROID +void CrashReporter::writeSystemInfo() +{ + // Get 'model name' and 'flags' from /proc/cpuinfo. + if( std::ifstream cpuinfo( "/proc/cpuinfo" ); cpuinfo ) + { + bool haveModel = false; + bool haveFlags = false; + std::regex modelRegex( "^model name[ \t]*:[ \t]*(.*)$" ); + std::regex flagsRegex( "^flags[ \t]*:[ \t]*(.*)$" ); + for( std::string line; std::getline( cpuinfo, line ); ) + { + std::smatch match; + if( !haveModel && std::regex_match( line, match, modelRegex ) && match.size() == 2) + { + addKeyValue("CPUModelName", OUString::fromUtf8( match[ 1 ].str()), AddItem); + haveModel = true; + } + if( !haveFlags && std::regex_match( line, match, flagsRegex ) && match.size() == 2) + { + addKeyValue("CPUFlags", OUString::fromUtf8( match[ 1 ].str()), AddItem); + haveFlags = true; + } + if( haveModel && haveFlags ) + break; + } + } + // Get 'MemTotal' from /proc/meminfo. + if( std::ifstream meminfo( "/proc/meminfo" ); meminfo ) + { + std::regex memTotalRegex( "^MemTotal[ \t]*:[ \t]*(.*)$" ); + for( std::string line; std::getline( meminfo, line ); ) + { + std::smatch match; + if( std::regex_match( line, match, memTotalRegex ) && match.size() == 2) + { + addKeyValue("MemoryTotal", OUString::fromUtf8( match[ 1 ].str()), AddItem); + break; + } + } + } +} +#elif defined _WIN32 +void CrashReporter::writeSystemInfo() +{ +#if !defined(_ARM64_) + // Get CPU model name and flags. + // See https://docs.microsoft.com/en-us/cpp/intrinsics/cpuid-cpuidex + // and https://en.wikipedia.org/wiki/CPUID . + int cpui[ 4 ]; + __cpuid( cpui, 0x80000000 ); // Get the highest extended ID. + unsigned int exIds = cpui[ 0 ]; + if( exIds >= 0x80000004 ) + { + int brand[ 16 ]; + __cpuidex( brand, 0x80000002, 0 ); + __cpuidex( brand + 4, 0x80000003, 0 ); + __cpuidex( brand + 8, 0x80000004, 0 ); + brand[ 12 ] = 0;; + addKeyValue( "CPUModelName", OUString::fromUtf8( reinterpret_cast< const char* >( brand )), + AddItem ); + } + __cpuid( cpui, 0 ); // Get the highest ID. + int ids = cpui[ 0 ]; + unsigned int ecx1 = 0, edx1 = 0, ebx7 = 0, ecx7 = 0, ecx81 = 0, edx81 = 0; + if( ids >= 0x1 ) + { + __cpuidex( cpui, 0x1, 0 ); + ecx1 = cpui[ 2 ]; + edx1 = cpui[ 3 ]; + } + if( ids >= 0x7 ) + { + __cpuidex( cpui, 0x7, 0 ); + ebx7 = cpui[ 1 ]; + ecx7 = cpui[ 2 ]; + } + if( exIds >= 0x80000001 ) + { + __cpuidex( cpui, 0x80000001, 0 ); + ecx81 = cpui[ 2 ]; + edx81 = cpui[ 3 ]; + } + struct FlagItem + { + unsigned int* reg; + int bit; + const char* name; + }; + const FlagItem flagItems[] = + { + { &ecx1, 0, "sse3" }, + { &ecx1, 1, "pclmulqdq" }, + { &ecx1, 3, "monitor" }, + { &ecx1, 9, "ssse3" }, + { &ecx1, 12, "fma" }, + { &ecx1, 13, "cpmxch16b" }, + { &ecx1, 19, "sse41" }, + { &ecx1, 20, "sse42" }, + { &ecx1, 22, "movbe" }, + { &ecx1, 23, "popcnt" }, + { &ecx1, 25, "aes" }, + { &ecx1, 26, "xsave" }, + { &ecx1, 27, "osxsave" }, + { &ecx1, 28, "avx" }, + { &ecx1, 29, "f16c" }, + { &ecx1, 30, "rdrand" }, + { &edx1, 5, "msr" }, + { &edx1, 8, "cx8" }, + { &edx1, 11, "sep" }, + { &edx1, 15, "cmov" }, + { &edx1, 19, "clfsh" }, + { &edx1, 23, "mmx" }, + { &edx1, 24, "fxsr" }, + { &edx1, 25, "sse" }, + { &edx1, 26, "sse2" }, + { &edx1, 28, "ht" }, + { &ebx7, 0, "fsgsbase" }, + { &ebx7, 3, "bmi1" }, + { &ebx7, 4, "hle" }, + { &ebx7, 5, "avx2" }, + { &ebx7, 8, "bmi2" }, + { &ebx7, 9, "erms" }, + { &ebx7, 10, "invpcid" }, + { &ebx7, 11, "rtm" }, + { &ebx7, 16, "avx512f" }, + { &ebx7, 18, "rdseed" }, + { &ebx7, 19, "adx" }, + { &ebx7, 26, "avx512pf" }, + { &ebx7, 27, "avx512er" }, + { &ebx7, 28, "avx512cd" }, + { &ebx7, 29, "sha" }, + { &ecx7, 0, "prefetchwt1" }, + { &ecx81, 0, "lahf" }, + { &ecx81, 5, "abm" }, + { &ecx81, 6, "sse4a" }, + { &ecx81, 11, "xop" }, + { &ecx81, 21, "tbm" }, + { &edx81, 11, "syscall" }, + { &edx81, 22, "mmxext" }, + { &edx81, 27, "rdtscp" }, + { &edx81, 30, "3dnowext" }, + { &edx81, 31, "3dnow" } + }; + OUStringBuffer flags; + for( const FlagItem& item : flagItems ) + { + if( *item.reg & ( 1U << item.bit )) + { + if( !flags.isEmpty()) + flags.append( " " ); + flags.appendAscii( item.name ); + } + } + if( !flags.isEmpty()) + addKeyValue( "CPUFlags", flags.makeStringAndClear(), AddItem ); +#endif + // Get total memory. + MEMORYSTATUSEX memoryStatus; + memoryStatus.dwLength = sizeof( memoryStatus ); + if( GlobalMemoryStatusEx( &memoryStatus )) + { + addKeyValue( "MemoryTotal", OUString::number( int( memoryStatus.ullTotalPhys / 1024 )) + + " kB", AddItem ); + } +} +#else +void CrashReporter::writeSystemInfo() +{ +} +#endif + +#endif //HAVE_FEATURE_BREAKPAD + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/desktopcontext.cxx b/desktop/source/app/desktopcontext.cxx new file mode 100644 index 000000000..5d43bb648 --- /dev/null +++ b/desktop/source/app/desktopcontext.cxx @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <config_java.h> + +#include "desktopcontext.hxx" + +#include <svtools/javainteractionhandler.hxx> + +using namespace com::sun::star::uno; +using namespace com::sun::star::task; + +namespace desktop +{ +DesktopContext::DesktopContext(const Reference<XCurrentContext>& ctx) + : m_xNextContext(ctx) +{ +} + +Any SAL_CALL DesktopContext::getValueByName(const OUString& Name) +{ + Any retVal; + + if (Name == JAVA_INTERACTION_HANDLER_NAME) + { +#if HAVE_FEATURE_JAVA + retVal <<= Reference<XInteractionHandler>(new svt::JavaInteractionHandler()); +#endif + } + else if (m_xNextContext.is()) + { + // Call next context in chain if found + retVal = m_xNextContext->getValueByName(Name); + } + return retVal; +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/desktopcontext.hxx b/desktop/source/app/desktopcontext.hxx new file mode 100644 index 000000000..42266a072 --- /dev/null +++ b/desktop/source/app/desktopcontext.hxx @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <cppuhelper/implbase.hxx> +#include <com/sun/star/uno/XCurrentContext.hpp> + +namespace desktop +{ + class DesktopContext: public cppu::WeakImplHelper< css::uno::XCurrentContext > + { + public: + explicit DesktopContext( const css::uno::Reference< css::uno::XCurrentContext > & ctx); + + // XCurrentContext + virtual css::uno::Any SAL_CALL getValueByName( const OUString& Name ) override; + + private: + css::uno::Reference< css::uno::XCurrentContext > m_xNextContext; + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/dispatchwatcher.cxx b/desktop/source/app/dispatchwatcher.cxx new file mode 100644 index 000000000..381507149 --- /dev/null +++ b/desktop/source/app/dispatchwatcher.cxx @@ -0,0 +1,832 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <sal/log.hxx> +#include <sfx2/docfile.hxx> +#include <sfx2/docfilt.hxx> +#include <sfx2/fcontnr.hxx> +#include <svl/fstathelper.hxx> + +#include <app.hxx> +#include "dispatchwatcher.hxx" +#include "officeipcthread.hxx" +#include <rtl/ustring.hxx> +#include <comphelper/processfactory.hxx> +#include <comphelper/synchronousdispatch.hxx> +#include <com/sun/star/io/IOException.hpp> +#include <com/sun/star/util/XCloseable.hpp> +#include <com/sun/star/util/CloseVetoException.hpp> +#include <com/sun/star/task/InteractionHandler.hpp> +#include <com/sun/star/util/URL.hpp> +#include <com/sun/star/frame/Desktop.hpp> +#include <com/sun/star/container/XContainerQuery.hpp> +#include <com/sun/star/container/XEnumeration.hpp> +#include <com/sun/star/frame/XDispatch.hpp> +#include <com/sun/star/frame/XNotifyingDispatch.hpp> +#include <com/sun/star/beans/PropertyValue.hpp> +#include <com/sun/star/view/XPrintable.hpp> +#include <com/sun/star/util/URLTransformer.hpp> +#include <com/sun/star/util/XURLTransformer.hpp> +#include <com/sun/star/document/MacroExecMode.hpp> +#include <com/sun/star/document/XTypeDetection.hpp> +#include <com/sun/star/document/UpdateDocMode.hpp> +#include <com/sun/star/frame/XStorable.hpp> +#include <com/sun/star/script/XLibraryContainer2.hpp> +#include <com/sun/star/document/XEmbeddedScripts.hpp> + +#include <comphelper/propertyvalue.hxx> +#include <comphelper/sequence.hxx> +#include <tools/diagnose_ex.h> +#include <tools/urlobj.hxx> +#include <unotools/mediadescriptor.hxx> +#include <unotools/tempfile.hxx> + +#include <osl/thread.hxx> +#include <osl/file.hxx> +#include <iostream> +#include <string_view> +#include <utility> + +using namespace ::osl; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::util; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::frame; +using namespace ::com::sun::star::container; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::view; +using namespace ::com::sun::star::task; +using namespace ::com::sun::star::document; + +namespace document = ::com::sun::star::document; + +namespace desktop +{ + +namespace { + +struct DispatchHolder +{ + DispatchHolder( URL _aURL, Reference< XDispatch > const & rDispatch ) : + aURL(std::move( _aURL )), xDispatch( rDispatch ) {} + + URL aURL; + Reference< XDispatch > xDispatch; +}; + +std::shared_ptr<const SfxFilter> impl_lookupExportFilterForUrl( std::u16string_view rUrl, std::u16string_view rFactory ) +{ + // create the list of filters + OUString sQuery = "getSortedFilterList()" + ":module=" + + OUString::Concat(rFactory) + // use long name here ! + ":iflags=" + + OUString::number(static_cast<sal_Int32>(SfxFilterFlags::EXPORT)) + + ":eflags=" + + OUString::number(static_cast<int>(SFX_FILTER_NOTINSTALLED)); + + const Reference< XComponentContext > xContext( comphelper::getProcessComponentContext() ); + const Reference< XContainerQuery > xFilterFactory( + xContext->getServiceManager()->createInstanceWithContext( "com.sun.star.document.FilterFactory", xContext ), + UNO_QUERY_THROW ); + + std::shared_ptr<const SfxFilter> pBestMatch; + + const Reference< XEnumeration > xFilterEnum( + xFilterFactory->createSubSetEnumerationByQuery( sQuery ), UNO_SET_THROW ); + while ( xFilterEnum->hasMoreElements() ) + { + comphelper::SequenceAsHashMap aFilterProps( xFilterEnum->nextElement() ); + const OUString aName( aFilterProps.getUnpackedValueOrDefault( "Name", OUString() ) ); + if ( !aName.isEmpty() ) + { + std::shared_ptr<const SfxFilter> pFilter( SfxFilter::GetFilterByName( aName ) ); + if ( pFilter && pFilter->CanExport() && pFilter->GetWildcard().Matches( rUrl ) ) + { + if ( !pBestMatch || ( SfxFilterFlags::PREFERED & pFilter->GetFilterFlags() ) ) + pBestMatch = pFilter; + } + } + } + + return pBestMatch; +} + +std::shared_ptr<const SfxFilter> impl_getExportFilterFromUrl( + const OUString& rUrl, const OUString& rFactory) +{ + try + { + const Reference< XComponentContext > xContext( comphelper::getProcessComponentContext() ); + const Reference< document::XTypeDetection > xTypeDetector( + xContext->getServiceManager()->createInstanceWithContext( "com.sun.star.document.TypeDetection", xContext ), + UNO_QUERY_THROW ); + const OUString aTypeName( xTypeDetector->queryTypeByURL( rUrl ) ); + + std::shared_ptr<const SfxFilter> pFilter( SfxFilterMatcher( rFactory ).GetFilter4EA( aTypeName, SfxFilterFlags::EXPORT ) ); + if ( !pFilter ) + pFilter = impl_lookupExportFilterForUrl( rUrl, rFactory ); + if ( !pFilter ) + { + OUString aTempName; + FileBase::getSystemPathFromFileURL( rUrl, aTempName ); + OString aSource = OUStringToOString ( aTempName, osl_getThreadTextEncoding() ); + std::cerr << "Error: no export filter for " << aSource << " found, aborting." << std::endl; + } + + return pFilter; + } + catch ( const Exception& ) + { + return nullptr; + } +} + +OUString impl_GuessFilter( const OUString& rUrlOut, const OUString& rDocService ) +{ + OUString aOutFilter; + std::shared_ptr<const SfxFilter> pOutFilter = impl_getExportFilterFromUrl( rUrlOut, rDocService ); + if (pOutFilter) + aOutFilter = pOutFilter->GetFilterName(); + + return aOutFilter; +} + +/// dump scripts in a document to the console. +void scriptCat(const Reference< XModel >& xDoc ) +{ + Reference< XEmbeddedScripts > xScriptAccess( xDoc, UNO_QUERY ); + if (!xScriptAccess) + { + std::cout << "No script access\n"; + return; + } + + // ignore xScriptAccess->getDialogLibraries() for now + Reference< css::script::XLibraryContainer2 > xLibraries( + xScriptAccess->getBasicLibraries() ); + + if ( !xLibraries.is() ) + { + std::cout << "No script libraries\n"; + return; + } + + const Sequence< OUString > aLibNames = xLibraries->getElementNames(); + std::cout << "Libraries: " << aLibNames.getLength() << "\n"; + for (OUString const & libName : aLibNames) + { + std::cout << "Library: '" << libName << "' children: "; + Reference< XNameContainer > xContainer; + try { + if (!xLibraries->isLibraryLoaded( libName )) + xLibraries->loadLibrary( libName ); + xContainer = Reference< XNameContainer >( + xLibraries->getByName( libName ), UNO_QUERY ); + } + catch (const css::uno::Exception &e) + { + std::cout << "[" << libName << "] - failed to load library: " << e.Message << "\n"; + continue; + } + if( !xContainer.is() ) + std::cout << "0\n"; + else + { + Sequence< OUString > aObjectNames = xContainer->getElementNames(); + + std::cout << aObjectNames.getLength() << "\n\n"; + for ( sal_Int32 j = 0 ; j < aObjectNames.getLength() ; ++j ) + { + const OUString &rObjectName = aObjectNames[j]; + + try + { + Any aCode = xContainer->getByName( rObjectName ); + OUString aCodeString; + + if (! (aCode >>= aCodeString ) ) + std::cout << "[" << rObjectName << "] - error fetching code\n"; + else + std::cout << "[" << rObjectName << "]\n" + << aCodeString.trim() + << "\n[/" << rObjectName << "]\n"; + } + catch (const css::uno::Exception &e) + { + std::cout << "[" << rObjectName << "] - exception " << e.Message << " fetching code\n"; + } + + if (j < aObjectNames.getLength() - 1) + std::cout << "\n----------------------------------------------------------\n"; + std::cout << "\n"; + } + } + } +} + +// Perform batch print +void batchPrint( std::u16string_view rPrinterName, const Reference< XPrintable > &xDoc, + const INetURLObject &aObj, const OUString &aName ) +{ + OUString aFilterOut; + OUString aPrinterName; + size_t nPathIndex = rPrinterName.rfind( ';' ); + if( nPathIndex != std::u16string_view::npos ) + aFilterOut=rPrinterName.substr( nPathIndex+1 ); + if( nPathIndex != 0 ) + aPrinterName=rPrinterName.substr( 0, nPathIndex ); + + INetURLObject aOutFilename( aObj ); + aOutFilename.SetExtension( u"pdf" ); + FileBase::getFileURLFromSystemPath( aFilterOut, aFilterOut ); + OUString aOutFile = aFilterOut + "/" + aOutFilename.getName(); + + OUString aTempName; + FileBase::getSystemPathFromFileURL( aName, aTempName ); + OString aSource8 = OUStringToOString ( aTempName, osl_getThreadTextEncoding() ); + FileBase::getSystemPathFromFileURL( aOutFile, aTempName ); + OString aTargetURL8 = OUStringToOString(aTempName, osl_getThreadTextEncoding() ); + + std::cout << "print " << aSource8 << " -> " << aTargetURL8; + std::cout << " using " << (aPrinterName.isEmpty() ? "<default_printer>" : OUStringToOString( aPrinterName, osl_getThreadTextEncoding() )); + std::cout << std::endl; + + // create the custom printer, if given + Sequence < PropertyValue > aPrinterArgs; + if( !aPrinterName.isEmpty() ) + { + aPrinterArgs = { comphelper::makePropertyValue("Name", aPrinterName) }; + xDoc->setPrinter( aPrinterArgs ); + } + + // print ( also without user interaction ) + aPrinterArgs = { comphelper::makePropertyValue("FileName", aOutFile), + comphelper::makePropertyValue("Wait", true) }; + xDoc->print( aPrinterArgs ); +} + +} // anonymous namespace + +DispatchWatcher::DispatchWatcher() + : m_nRequestCount(0) +{ +} + + +DispatchWatcher::~DispatchWatcher() +{ +} + + +bool DispatchWatcher::executeDispatchRequests( const std::vector<DispatchRequest>& aDispatchRequestsList, bool bNoTerminate ) +{ + Reference< XDesktop2 > xDesktop = css::frame::Desktop::create( ::comphelper::getProcessComponentContext() ); + + std::vector< DispatchHolder > aDispatches; + bool bSetInputFilter = false; + OUString aForcedInputFilter; + + for (auto const & aDispatchRequest: aDispatchRequestsList) + { + // Set Input Filter + if ( aDispatchRequest.aRequestType == REQUEST_INFILTER ) + { + bSetInputFilter = true; + aForcedInputFilter = aDispatchRequest.aURL; + RequestHandler::RequestsCompleted(); + continue; + } + + // create parameter array + std::vector<PropertyValue> aArgs; + + // mark request as user interaction from outside + aArgs.emplace_back("Referer", 0, Any(OUString("private:OpenEvent")), + PropertyState_DIRECT_VALUE); + + OUString aTarget("_default"); + + if ( aDispatchRequest.aRequestType == REQUEST_PRINT || + aDispatchRequest.aRequestType == REQUEST_PRINTTO || + aDispatchRequest.aRequestType == REQUEST_BATCHPRINT || + aDispatchRequest.aRequestType == REQUEST_CONVERSION || + aDispatchRequest.aRequestType == REQUEST_CAT || + aDispatchRequest.aRequestType == REQUEST_SCRIPT_CAT) + { + // documents opened for printing are opened readonly because they must be opened as a + // new document and this document could be open already + aArgs.emplace_back("ReadOnly", 0, Any(true), PropertyState_DIRECT_VALUE); + // always open a new document for printing, because it must be disposed afterwards + aArgs.emplace_back("OpenNewView", 0, Any(true), PropertyState_DIRECT_VALUE); + // printing is done in a hidden view + aArgs.emplace_back("Hidden", 0, Any(true), PropertyState_DIRECT_VALUE); + // load document for printing without user interaction + aArgs.emplace_back("Silent", 0, Any(true), PropertyState_DIRECT_VALUE); + + // hidden documents should never be put into open tasks + aTarget = "_blank"; + } + else + { + Reference < XInteractionHandler2 > xInteraction( + InteractionHandler::createWithParent(::comphelper::getProcessComponentContext(), nullptr) ); + + aArgs.emplace_back("InteractionHandler", 0, Any(xInteraction), + PropertyState_DIRECT_VALUE); + + aArgs.emplace_back("MacroExecutionMode", 0, + Any(css::document::MacroExecMode::USE_CONFIG), + PropertyState_DIRECT_VALUE); + + aArgs.emplace_back("UpdateDocMode", 0, + Any(css::document::UpdateDocMode::ACCORDING_TO_CONFIG), + PropertyState_DIRECT_VALUE); + } + + if ( !aDispatchRequest.aPreselectedFactory.isEmpty() ) + { + aArgs.emplace_back(utl::MediaDescriptor::PROP_DOCUMENTSERVICE, 0, + Any(aDispatchRequest.aPreselectedFactory), + PropertyState_DIRECT_VALUE); + } + + OUString aName( GetURL_Impl( aDispatchRequest.aURL, aDispatchRequest.aCwdUrl ) ); + + // load the document ... if they are loadable! + // Otherwise try to dispatch it ... + Reference < XPrintable > xDoc; + if( + ( aName.startsWith( ".uno" ) ) || + ( aName.startsWith( "slot:" ) ) || + ( aName.startsWith( "macro:" ) ) || + ( aName.startsWith("vnd.sun.star.script") ) + ) + { + // Attention: URL must be parsed full. Otherwise some detections on it will fail! + // It doesn't matter, if parser isn't available. Because; We try loading of URL then ... + URL aURL ; + aURL.Complete = aName; + + Reference < XDispatch > xDispatcher ; + Reference < XURLTransformer > xParser ( URLTransformer::create(::comphelper::getProcessComponentContext()) ); + + if( xParser.is() ) + xParser->parseStrict( aURL ); + + xDispatcher = xDesktop->queryDispatch( aURL, OUString(), 0 ); + SAL_WARN_IF( + !xDispatcher.is(), "desktop.app", + "unsupported dispatch request <" << aName << ">"); + if( xDispatcher.is() ) + { + { + osl::MutexGuard aGuard(m_mutex); + // Remember request so we can find it in statusChanged! + m_nRequestCount++; + } + + // Use local vector to store dispatcher because we have to fill our request container before + // we can dispatch. Otherwise it would be possible that statusChanged is called before we dispatched all requests!! + aDispatches.emplace_back( aURL, xDispatcher ); + } + } + else if ( aName.startsWith( "service:" ) ) + { + // TODO: the dispatch has to be done for loadComponentFromURL as well. + URL aURL ; + aURL.Complete = aName; + + Reference < XDispatch > xDispatcher ; + Reference < XURLTransformer > xParser ( URLTransformer::create(::comphelper::getProcessComponentContext()) ); + + if( xParser.is() ) + xParser->parseStrict( aURL ); + + xDispatcher = xDesktop->queryDispatch( aURL, OUString(), 0 ); + + if( xDispatcher.is() ) + { + try + { + // We have to be listener to catch errors during dispatching URLs. + // Otherwise it would be possible to have an office running without an open + // window!! + Sequence < PropertyValue > aArgs2{ comphelper::makePropertyValue("SynchronMode", + true) }; + Reference < XNotifyingDispatch > xDisp( xDispatcher, UNO_QUERY ); + if ( xDisp.is() ) + xDisp->dispatchWithNotification( aURL, aArgs2, this ); + else + xDispatcher->dispatch( aURL, aArgs2 ); + } + catch (const css::uno::Exception&) + { + TOOLS_WARN_EXCEPTION( + "desktop.app", + "Desktop::OpenDefault() ignoring Exception while calling XNotifyingDispatch"); + } + } + } + else + { + INetURLObject aObj( aName ); + if ( aObj.GetProtocol() == INetProtocol::PrivSoffice ) + aTarget = "_default"; + + // Set "AsTemplate" argument according to request type + if ( aDispatchRequest.aRequestType == REQUEST_FORCENEW || + aDispatchRequest.aRequestType == REQUEST_FORCEOPEN ) + { + aArgs.emplace_back("AsTemplate", 0, + Any(aDispatchRequest.aRequestType == REQUEST_FORCENEW), + PropertyState_DIRECT_VALUE); + } + + // if we are called in viewmode, open document read-only + if(aDispatchRequest.aRequestType == REQUEST_VIEW) { + aArgs.emplace_back("ReadOnly", 0, Any(true), PropertyState_DIRECT_VALUE); + } + + // if we are called with --show set Start in mediadescriptor + if(aDispatchRequest.aRequestType == REQUEST_START) { + aArgs.emplace_back("StartPresentation", 0, Any(true), PropertyState_DIRECT_VALUE); + } + + // Force input filter, if possible + if( bSetInputFilter ) + { + sal_Int32 nFilterOptionsIndex = 0; + aArgs.emplace_back("FilterName", 0, + Any(aForcedInputFilter.getToken(0, ':', nFilterOptionsIndex)), + PropertyState_DIRECT_VALUE); + + if (0 < nFilterOptionsIndex) + { + aArgs.emplace_back("FilterOptions", 0, + Any(aForcedInputFilter.copy(nFilterOptionsIndex)), + PropertyState_DIRECT_VALUE); + } + } + + // This is a synchron loading of a component so we don't have to deal with our statusChanged listener mechanism. + try + { + xDoc.set(comphelper::SynchronousDispatch::dispatch( + xDesktop, aName, aTarget, comphelper::containerToSequence(aArgs)), + UNO_QUERY); + } + catch (const css::lang::IllegalArgumentException&) + { + TOOLS_WARN_EXCEPTION( + "desktop.app", + "Dispatchwatcher IllegalArgumentException while calling loadComponentFromURL"); + } + catch (const css::io::IOException&) + { + TOOLS_WARN_EXCEPTION( + "desktop.app", + "Dispatchwatcher IOException while calling loadComponentFromURL"); + } + if ( aDispatchRequest.aRequestType == REQUEST_OPEN || + aDispatchRequest.aRequestType == REQUEST_VIEW || + aDispatchRequest.aRequestType == REQUEST_START || + aDispatchRequest.aRequestType == REQUEST_FORCEOPEN || + aDispatchRequest.aRequestType == REQUEST_FORCENEW ) + { + // request is completed + RequestHandler::RequestsCompleted(); + } + else if ( aDispatchRequest.aRequestType == REQUEST_PRINT || + aDispatchRequest.aRequestType == REQUEST_PRINTTO || + aDispatchRequest.aRequestType == REQUEST_BATCHPRINT || + aDispatchRequest.aRequestType == REQUEST_CONVERSION || + aDispatchRequest.aRequestType == REQUEST_CAT || + aDispatchRequest.aRequestType == REQUEST_SCRIPT_CAT ) + { + if ( xDoc.is() ) + { + // Do we need to save the document in a different format? + if ( aDispatchRequest.aRequestType == REQUEST_CONVERSION || + aDispatchRequest.aRequestType == REQUEST_CAT ) + { +// FIXME: factor out into a method ... + Reference< XStorable > xStorable( xDoc, UNO_QUERY ); + if ( xStorable.is() ) { + OUString aParam = aDispatchRequest.aPrinterName; + sal_Int32 nPathIndex = aParam.lastIndexOf( ';' ); + sal_Int32 nFilterIndex = aParam.indexOf( ':' ); + sal_Int32 nImgFilterIndex = aParam.lastIndexOf( '|' ); + if( nPathIndex < nFilterIndex ) + nFilterIndex = -1; + + OUString aFilterOut; + OUString aImgOut; + OUString aFilter; + OUString aFilterExt; + bool bGuess = false; + + if( nFilterIndex >= 0 ) + { + aFilter = aParam.copy( nFilterIndex+1, nPathIndex-nFilterIndex-1 ); + aFilterExt = aParam.copy( 0, nFilterIndex ); + } + else + { + // Guess + bGuess = true; + aFilterExt = aParam.copy( 0, nPathIndex ); + } + + if( nImgFilterIndex >= 0 ) + { + aImgOut = aParam.copy( nImgFilterIndex+1 ); + aFilterOut = aParam.copy( nPathIndex+1, nImgFilterIndex-nPathIndex-1 ); + } + else + aFilterOut = aParam.copy( nPathIndex+1 ); + + FileBase::getFileURLFromSystemPath( aFilterOut, aFilterOut ); + INetURLObject aOutFilename(aFilterOut); + aOutFilename.Append(aObj.getName(INetURLObject::LAST_SEGMENT, true, + INetURLObject::DecodeMechanism::NONE)); + aOutFilename.SetExtension(aFilterExt); + OUString aOutFile + = aOutFilename.GetMainURL(INetURLObject::DecodeMechanism::NONE); + + std::unique_ptr<utl::TempFile> fileForCat; + if( aDispatchRequest.aRequestType == REQUEST_CAT ) + { + fileForCat = std::make_unique<utl::TempFile>(); + if (fileForCat->IsValid()) + fileForCat->EnableKillingFile(); + else + std::cerr << "Error: Cannot create temporary file..." << std::endl ; + aOutFile = fileForCat->GetURL(); + } + + if ( bGuess ) + { + OUString aDocService; + Reference< XModel > xModel( xDoc, UNO_QUERY ); + if ( xModel.is() ) + { + utl::MediaDescriptor aMediaDesc( xModel->getArgs() ); + aDocService = aMediaDesc.getUnpackedValueOrDefault(utl::MediaDescriptor::PROP_DOCUMENTSERVICE, OUString() ); + } + aFilter = impl_GuessFilter( aOutFile, aDocService ); + } + + bool bMultiFileTarget = false; + + if (aFilter.isEmpty()) + { + std::cerr << "Error: no export filter" << std::endl; + } + else + { + sal_Int32 nFilterOptionsIndex = aFilter.indexOf(':'); + sal_Int32 nProps = ( 0 < nFilterOptionsIndex ) ? 4 : 3; + + if ( !aImgOut.isEmpty() ) + nProps +=1; + Sequence<PropertyValue> conversionProperties( nProps ); + auto pconversionProperties = conversionProperties.getArray(); + pconversionProperties[0].Name = "ConversionRequestOrigin"; + pconversionProperties[0].Value <<= OUString("CommandLine"); + pconversionProperties[1].Name = "Overwrite"; + pconversionProperties[1].Value <<= true; + + pconversionProperties[2].Name = "FilterName"; + if( 0 < nFilterOptionsIndex ) + { + OUString sFilterName = aFilter.copy(0, nFilterOptionsIndex); + OUString sFilterOptions = aFilter.copy(nFilterOptionsIndex + 1); + + if (sFilterName == "Text - txt - csv (StarCalc)") + { + sal_Int32 nIdx(0); + // If the 11th token is '-1' then we export a file + // per sheet where the file name is based on the suggested + // output filename concatenated with the sheet name, so adjust + // the output and overwrite messages + // If the 11th token is not present or numeric 0 then the + // default sheet is exported with the output filename. If it + // is numeric >0 then that sheet (1-based) with the output + // filename concatenated with the sheet name. So even if + // that is a single file, the multi file target mechanism is + // used. + const OUString aTok(sFilterOptions.getToken(11, ',', nIdx)); + // Actual validity is checked in Calc, here just check for + // presence of numeric value at start. + bMultiFileTarget = (!aTok.isEmpty() && aTok.toInt32() != 0); + } + + pconversionProperties[2].Value <<= sFilterName; + + pconversionProperties[3].Name = "FilterOptions"; + pconversionProperties[3].Value <<= sFilterOptions; + } + else + { + pconversionProperties[2].Value <<= aFilter; + } + + if ( !aImgOut.isEmpty() ) + { + assert(conversionProperties[nProps-1].Name.isEmpty()); + pconversionProperties[nProps-1].Name = "ImageFilter"; + pconversionProperties[nProps-1].Value <<= aImgOut; + } + + OUString aTempName; + FileBase::getSystemPathFromFileURL(aName, aTempName); + OString aSource8 = OUStringToOString(aTempName, osl_getThreadTextEncoding()); + FileBase::getSystemPathFromFileURL(aOutFile, aTempName); + OString aTargetURL8 = OUStringToOString(aTempName, osl_getThreadTextEncoding()); + if (aDispatchRequest.aRequestType != REQUEST_CAT) + { + std::cout << "convert " << aSource8; + if (!bMultiFileTarget) + std::cout << " -> " << aTargetURL8; + std::cout << " using filter : " << OUStringToOString(aFilter, osl_getThreadTextEncoding()) << std::endl; + if (!bMultiFileTarget && FStatHelper::IsDocument(aOutFile)) + std::cout << "Overwriting: " << OUStringToOString(aTempName, osl_getThreadTextEncoding()) << std::endl ; + } + try + { + xStorable->storeToURL(aOutFile, conversionProperties); + } + catch (const Exception& rException) + { + std::cerr << "Error: Please verify input parameters..."; + if (!rException.Message.isEmpty()) + std::cerr << " (" << rException.Message << ")"; + std::cerr << std::endl; + } + + if (fileForCat && fileForCat->IsValid()) + { + SvStream* aStream = fileForCat->GetStream(StreamMode::STD_READ); + while (aStream->good()) + { + OString aStr; + aStream->ReadLine(aStr, SAL_MAX_INT32); + for (sal_Int32 i = 0; i < aStr.getLength(); ++i) + { + std::cout << aStr[i]; + } + std::cout << std::endl; + } + } + } + } + } + else if ( aDispatchRequest.aRequestType == REQUEST_SCRIPT_CAT ) + { + Reference< XModel > xModel( xDoc, UNO_QUERY ); + if( xModel.is() ) + scriptCat( xModel ); + } + else if ( aDispatchRequest.aRequestType == REQUEST_BATCHPRINT ) + { + batchPrint( aDispatchRequest.aPrinterName, xDoc, aObj, aName ); + } + else + { + if ( aDispatchRequest.aRequestType == REQUEST_PRINTTO ) + { + // create the printer + Sequence < PropertyValue > aPrinterArgs{ comphelper::makePropertyValue( + "Name", aDispatchRequest.aPrinterName) }; + xDoc->setPrinter( aPrinterArgs ); + } + + // print ( also without user interaction ) + Sequence < PropertyValue > aPrinterArgs{ comphelper::makePropertyValue("Wait", + true) }; + xDoc->print( aPrinterArgs ); + } + } + else + { + std::cerr << "Error: source file could not be loaded" << std::endl; + } + + // remove the document + try + { + Reference < XCloseable > xClose( xDoc, UNO_QUERY ); + if ( xClose.is() ) + xClose->close( true ); + else + { + Reference < XComponent > xComp( xDoc, UNO_QUERY ); + if ( xComp.is() ) + xComp->dispose(); + } + } + catch (const css::util::CloseVetoException&) + { + } + + // request is completed + RequestHandler::RequestsCompleted(); + } + } + } + + if ( !aDispatches.empty() ) + { + // Execute all asynchronous dispatches now after we placed them into our request container! + Sequence < PropertyValue > aArgs{ + comphelper::makePropertyValue("Referer", OUString("private:OpenEvent")), + comphelper::makePropertyValue("SynchronMode", true) + }; + + for (const DispatchHolder & aDispatche : aDispatches) + { + Reference< XDispatch > xDispatch = aDispatche.xDispatch; + Reference < XNotifyingDispatch > xDisp( xDispatch, UNO_QUERY ); + if ( xDisp.is() ) + xDisp->dispatchWithNotification( aDispatche.aURL, aArgs, this ); + else + { + { + osl::MutexGuard aGuard(m_mutex); + m_nRequestCount--; + } + xDispatch->dispatch( aDispatche.aURL, aArgs ); + } + } + } + + ::osl::ClearableMutexGuard aGuard(m_mutex); + bool bEmpty = (m_nRequestCount == 0); + aGuard.clear(); + + // No more asynchronous requests? + // The requests are removed from the request container after they called back to this + // implementation via statusChanged!! + if ( bEmpty && !bNoTerminate /*m_aRequestContainer.empty()*/ ) + { + // We have to check if we have an open task otherwise we have to shutdown the office. + Reference< XElementAccess > xList = xDesktop->getFrames(); + + if ( !xList->hasElements() ) + { + // We don't have any task open so we have to shutdown ourself!! + return xDesktop->terminate(); + } + } + + return false; +} + + +void SAL_CALL DispatchWatcher::disposing( const css::lang::EventObject& ) +{ +} + + +void SAL_CALL DispatchWatcher::dispatchFinished( const DispatchResultEvent& ) +{ + osl::ClearableMutexGuard aGuard(m_mutex); + sal_Int16 nCount = --m_nRequestCount; + aGuard.clear(); + RequestHandler::RequestsCompleted(); + if ( !nCount && !RequestHandler::AreRequestsPending() ) + { + // We have to check if we have an open task otherwise we have to shutdown the office. + Reference< XDesktop2 > xDesktop = css::frame::Desktop::create( ::comphelper::getProcessComponentContext() ); + Reference< XElementAccess > xList = xDesktop->getFrames(); + + if ( !xList->hasElements() ) + { + // We don't have any task open so we have to shutdown ourself!! + xDesktop->terminate(); + } + } +} + +} // namespace desktop + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/dispatchwatcher.hxx b/desktop/source/app/dispatchwatcher.hxx new file mode 100644 index 000000000..221cac915 --- /dev/null +++ b/desktop/source/app/dispatchwatcher.hxx @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <cppuhelper/implbase.hxx> +#include <com/sun/star/frame/XDispatchResultListener.hpp> +#include <optional> + +#include <vector> + +namespace desktop +{ + +/* + Class for controls dispatching of command URL through office command line. There + are "dangerous" command URLs, that can result in a running office without UI. To prevent + this situation the implementation monitors all dispatches and looks for an open task if + there is arose a problem. If there is none the office will be shutdown to prevent a + running office without UI. +*/ +class DispatchWatcher : public ::cppu::WeakImplHelper< css::frame::XDispatchResultListener > +{ + public: + enum RequestType + { + REQUEST_OPEN, + REQUEST_VIEW, + REQUEST_START, + REQUEST_PRINT, + REQUEST_PRINTTO, + REQUEST_FORCEOPEN, + REQUEST_FORCENEW, + REQUEST_CONVERSION, + REQUEST_INFILTER, + REQUEST_BATCHPRINT, + REQUEST_CAT, + REQUEST_SCRIPT_CAT + }; + + struct DispatchRequest + { + RequestType aRequestType; + OUString aURL; + std::optional< OUString > aCwdUrl; + OUString aPrinterName; // also conversion params + OUString aPreselectedFactory; + }; + + DispatchWatcher(); + + virtual ~DispatchWatcher() override; + + // XEventListener + virtual void SAL_CALL disposing( const css::lang::EventObject& Source ) override; + + // XDispachResultListener + virtual void SAL_CALL dispatchFinished( const css::frame::DispatchResultEvent& aEvent ) override; + + // execute new dispatch request + bool executeDispatchRequests( const std::vector<DispatchRequest>& aDispatches, bool bNoTerminate ); + + private: + osl::Mutex m_mutex; + + sal_Int16 m_nRequestCount; +}; + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/langselect.cxx b/desktop/source/app/langselect.cxx new file mode 100644 index 000000000..8a07e26c3 --- /dev/null +++ b/desktop/source/app/langselect.cxx @@ -0,0 +1,148 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <com/sun/star/configuration/theDefaultProvider.hpp> +#include <com/sun/star/container/XNameAccess.hpp> +#include <com/sun/star/lang/XLocalizable.hpp> +#include <com/sun/star/uno/Exception.hpp> +#include <com/sun/star/uno/Reference.hxx> +#include <com/sun/star/uno/Sequence.hxx> +#include <comphelper/configuration.hxx> +#include <comphelper/processfactory.hxx> +#include <i18nlangtag/lang.h> +#include <i18nlangtag/languagetag.hxx> +#include <i18nlangtag/mslangid.hxx> +#include <officecfg/Office/Linguistic.hxx> +#include <officecfg/Setup.hxx> +#include <officecfg/System.hxx> +#include <rtl/ustring.hxx> +#include <svl/languageoptions.hxx> +#include <svtools/langhelp.hxx> +#include <tools/diagnose_ex.h> + +#include <app.hxx> + +#include "cmdlineargs.hxx" +#include "langselect.hxx" + +namespace desktop::langselect { + +namespace { + +void setMsLangIdFallback(OUString const & locale) { + // #i32939# setting of default document language + // See #i42730# for rules for determining source of settings + if (locale.isEmpty()) + return; + + LanguageType type = LanguageTag::convertToLanguageTypeWithFallback(locale); + switch (SvtLanguageOptions::GetScriptTypeOfLanguage(type)) { + case SvtScriptType::ASIAN: + MsLangId::setConfiguredAsianFallback(type); + break; + case SvtScriptType::COMPLEX: + MsLangId::setConfiguredComplexFallback(type); + break; + default: + MsLangId::setConfiguredWesternFallback(type); + break; + } +} + +} + +bool prepareLocale() { + // #i42730# Get the windows 16Bit locale, it should be preferred over the UI + // locale: + setMsLangIdFallback(officecfg::System::L10N::SystemLocale::get()); + // #i32939# Use system locale to set document default locale: + setMsLangIdFallback(officecfg::System::L10N::Locale::get()); + css::uno::Sequence<OUString> inst( + officecfg::Setup::Office::InstalledLocales::get()->getElementNames()); + OUString locale(officecfg::Office::Linguistic::General::UILocale::get()); + if (!locale.isEmpty()) { + locale = getInstalledLocaleForLanguage(inst, locale); + if (locale.isEmpty()) { + // Selected language is not/no longer installed: + try { + std::shared_ptr<comphelper::ConfigurationChanges> batch( + comphelper::ConfigurationChanges::create()); + officecfg::Office::Linguistic::General::UILocale::set( + "", batch); + batch->commit(); + } catch (const css::uno::Exception &) { + TOOLS_WARN_EXCEPTION("desktop.app", "ignoring"); + } + } + } + if (locale.isEmpty()) { + locale = getInstalledLocaleForLanguage( + inst, Desktop::GetCommandLineArgs().GetLanguage()); + } + if (locale.isEmpty()) { + locale = getInstalledLocaleForSystemUILanguage(inst, true); + } + if (locale.isEmpty()) { + return false; + } + LanguageTag tag(locale); + // Prepare default config provider by localizing it to the selected + // locale this will ensure localized configuration settings to be + // selected according to the UI language: + css::uno::Reference<css::lang::XLocalizable>( + css::configuration::theDefaultProvider::get( + comphelper::getProcessComponentContext()), + css::uno::UNO_QUERY_THROW)->setLocale(tag.getLocale(false)); + try { + std::shared_ptr<comphelper::ConfigurationChanges> batch( + comphelper::ConfigurationChanges::create()); + officecfg::Setup::L10N::ooLocale::set(locale, batch); + batch->commit(); + } catch (const css::uno::Exception &) { + TOOLS_WARN_EXCEPTION("desktop.app", "ignoring"); + } + MsLangId::setConfiguredSystemUILanguage(tag.getLanguageType(false)); + + // Note the system language/locale here may or may not be correct before we + // actually set it below. It is what could be figured from the system + // setting and may only partially match, like "en" for an unsupported + // English locale. + LanguageTag aSysLocTag( MsLangId::getSystemLanguage()); + OUString setupSysLoc(officecfg::Setup::L10N::ooSetupSystemLocale::get()); + if (!setupSysLoc.isEmpty()) + aSysLocTag.reset( setupSysLoc); + // Ensure the system locale is set to a known supported locale. + aSysLocTag.makeFallback(); + LanguageTag::setConfiguredSystemLanguage( aSysLocTag.getLanguageType(false)); + + // #i32939# setting of default document locale + // #i32939# this should not be based on the UI language + // So obtain the system locale now configured just above and pass it on, + // resolved of course. + LanguageTag docTag(LANGUAGE_SYSTEM); + setMsLangIdFallback(docTag.getBcp47()); + + return true; +} + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/langselect.hxx b/desktop/source/app/langselect.hxx new file mode 100644 index 000000000..496f67570 --- /dev/null +++ b/desktop/source/app/langselect.hxx @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <sal/config.h> + +namespace desktop::langselect +{ +bool prepareLocale(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/lockfile2.cxx b/desktop/source/app/lockfile2.cxx new file mode 100644 index 000000000..44447927f --- /dev/null +++ b/desktop/source/app/lockfile2.cxx @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/svapp.hxx> +#include <vcl/weld.hxx> +#include <dp_shared.hxx> +#include <strings.hrc> +#include <tools/config.hxx> +#include <lockfile.hxx> + +namespace desktop { + +bool Lockfile_execWarning( Lockfile const * that ) +{ + // read information from lock + OUString aLockname = that->m_aLockname; + Config aConfig(aLockname); + aConfig.SetGroup( LOCKFILE_GROUP ); + OString aHost = aConfig.ReadKey( LOCKFILE_HOSTKEY ); + OString aUser = aConfig.ReadKey( LOCKFILE_USERKEY ); + OString aTime = aConfig.ReadKey( LOCKFILE_TIMEKEY ); + + // display warning and return response + std::unique_ptr<weld::MessageDialog> xBox(Application::CreateMessageDialog(nullptr, + VclMessageType::Question, VclButtonsType::YesNo, DpResId(STR_QUERY_USERDATALOCKED))); + // set box title + OUString aTitle = DpResId(STR_TITLE_USERDATALOCKED); + xBox->set_title( aTitle ); + // insert values... + OUString aMsgText = xBox->get_primary_text(); + aMsgText = aMsgText.replaceFirst( + "$u", OStringToOUString( aUser, RTL_TEXTENCODING_ASCII_US) ); + aMsgText = aMsgText.replaceFirst( + "$h", OStringToOUString( aHost, RTL_TEXTENCODING_ASCII_US) ); + aMsgText = aMsgText.replaceFirst( + "$t", OStringToOUString( aTime, RTL_TEXTENCODING_ASCII_US) ); + xBox->set_primary_text(aMsgText); + // do it + return xBox->run() == RET_YES; +} + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/main.c b/desktop/source/app/main.c new file mode 100644 index 000000000..fdd2eb350 --- /dev/null +++ b/desktop/source/app/main.c @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/main.h> + +#include "sofficemain.h" + +#ifndef NOTEST_xmlCleanupParser +#ifdef DBG_UTIL +#ifdef __gnu_linux__ +#include <stdio.h> +#include <stdlib.h> + +static int g_Exiting = 0; + +/* HACK: detect calls to xmlCleanupParser, which causes hard to debug crashes */ +__attribute__((visibility("default"))) void xmlCleanupParser(void) +{ + /* there are libraries that register xmlCleanupParser as an atexit handler, + which is not entirely sound (another atexit handler could want to + use libxml), but not enough of a problem to complain. + (example found by llunak: KDE's Strigi library) */ + if (!g_Exiting) + { + fprintf(stderr, "\n*** ERROR: DO NOT call xmlCleanupParser()\n\n"); + abort(); + } +} +#endif +#endif +#endif // NOTEST_xmlCleanupParser + +SAL_IMPLEMENT_MAIN() +{ + int ret = soffice_main(); +#ifndef NOTEST_xmlCleanupParser +#ifdef DBG_UTIL +#ifdef __gnu_linux__ + g_Exiting = 1; +#endif +#endif +#endif + return ret; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/officeipcthread.cxx b/desktop/source/app/officeipcthread.cxx new file mode 100644 index 000000000..e559f7253 --- /dev/null +++ b/desktop/source/app/officeipcthread.cxx @@ -0,0 +1,1369 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <config_dbus.h> +#include <config_features.h> +#include <config_feature_desktop.h> + +#include <app.hxx> +#include "officeipcthread.hxx" +#include "cmdlineargs.hxx" +#include "dispatchwatcher.hxx" +#include <com/sun/star/frame/TerminationVetoException.hpp> +#include <salhelper/thread.hxx> +#include <sal/log.hxx> +#include <unotools/bootstrap.hxx> +#include <utility> +#include <vcl/svapp.hxx> +#include <unotools/configmgr.hxx> +#include <osl/pipe.hxx> +#include <rtl/digest.h> +#include <rtl/ustrbuf.hxx> +#include <osl/conditn.hxx> +#include <unotools/moduleoptions.hxx> +#include <rtl/strbuf.hxx> +#include <cppuhelper/supportsservice.hxx> +#include <osl/file.hxx> +#include <rtl/process.h> +#include <o3tl/string_view.hxx> + +#include <cassert> +#include <cstdlib> +#include <memory> + +#if ENABLE_DBUS +#include <dbus/dbus.h> +#include <sys/socket.h> +#endif + +using namespace desktop; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::frame; + +namespace { + +char const ARGUMENT_PREFIX[] = "InternalIPC::Arguments"; +char const SEND_ARGUMENTS[] = "InternalIPC::SendArguments"; +char const PROCESSING_DONE[] = "InternalIPC::ProcessingDone"; + +// Receives packets from the pipe until a packet ends in a NUL character (that +// will not be included in the returned string) or it cannot read anything (due +// to error or closed pipe, in which case an empty string will be returned to +// signal failure): +OString readStringFromPipe(osl::StreamPipe const & pipe) { + for (OStringBuffer str;;) { + char buf[1024]; + sal_Int32 n = pipe.recv(buf, SAL_N_ELEMENTS(buf)); + if (n <= 0) { + SAL_INFO("desktop.app", "read empty string"); + return ""; + } + bool end = false; + if (buf[n - 1] == '\0') { + end = true; + --n; + } + str.append(buf, n); + //TODO: how does OStringBuffer.append handle overflow? + if (end) { + auto s = str.makeStringAndClear(); + SAL_INFO("desktop.app", "read <" << s << ">"); + return s; + } + } +} + +} + +namespace desktop +{ + +namespace { + +class Parser: public CommandLineArgs::Supplier { +public: + explicit Parser(OString input): m_input(std::move(input)) { + if (!m_input.match(ARGUMENT_PREFIX) || + m_input.getLength() == RTL_CONSTASCII_LENGTH(ARGUMENT_PREFIX)) + { + throw CommandLineArgs::Supplier::Exception(); + } + m_index = RTL_CONSTASCII_LENGTH(ARGUMENT_PREFIX); + switch (m_input[m_index++]) { + case '0': + break; + case '1': + { + OUString url; + if (!next(&url, false)) { + throw CommandLineArgs::Supplier::Exception(); + } + m_cwdUrl = url; + break; + } + case '2': + { + OUString path; + if (!next(&path, false)) { + throw CommandLineArgs::Supplier::Exception(); + } + OUString url; + if (osl::FileBase::getFileURLFromSystemPath(path, url) == + osl::FileBase::E_None) + { + m_cwdUrl = url; + } + break; + } + default: + throw CommandLineArgs::Supplier::Exception(); + } + } + + virtual std::optional< OUString > getCwdUrl() override { return m_cwdUrl; } + + virtual bool next(OUString * argument) override { return next(argument, true); } + +private: + bool next(OUString * argument, bool prefix) { + OSL_ASSERT(argument != nullptr); + if (m_index < m_input.getLength()) { + if (prefix) { + if (m_input[m_index] != ',') { + throw CommandLineArgs::Supplier::Exception(); + } + ++m_index; + } + OStringBuffer b; + while (m_index < m_input.getLength()) { + char c = m_input[m_index]; + if (c == ',') { + break; + } + ++m_index; + if (c == '\\') { + if (m_index >= m_input.getLength()) + throw CommandLineArgs::Supplier::Exception(); + c = m_input[m_index++]; + switch (c) { + case '0': + c = '\0'; + break; + case ',': + case '\\': + break; + default: + throw CommandLineArgs::Supplier::Exception(); + } + } + b.append(c); + } + OString b2(b.makeStringAndClear()); + if (!rtl_convertStringToUString( + &argument->pData, b2.getStr(), b2.getLength(), + RTL_TEXTENCODING_UTF8, + (RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_ERROR | + RTL_TEXTTOUNICODE_FLAGS_MBUNDEFINED_ERROR | + RTL_TEXTTOUNICODE_FLAGS_INVALID_ERROR))) + { + throw CommandLineArgs::Supplier::Exception(); + } + return true; + } else { + return false; + } + } + + std::optional< OUString > m_cwdUrl; + OString m_input; + sal_Int32 m_index; +}; + +bool addArgument(OStringBuffer &rArguments, char prefix, + const OUString &rArgument) +{ + OString utf8; + if (!rArgument.convertToString( + &utf8, RTL_TEXTENCODING_UTF8, + (RTL_UNICODETOTEXT_FLAGS_UNDEFINED_ERROR | + RTL_UNICODETOTEXT_FLAGS_INVALID_ERROR))) + { + return false; + } + rArguments.append(prefix); + for (sal_Int32 i = 0; i < utf8.getLength(); ++i) { + char c = utf8[i]; + switch (c) { + case '\0': + rArguments.append("\\0"); + break; + case ',': + rArguments.append("\\,"); + break; + case '\\': + rArguments.append("\\\\"); + break; + default: + rArguments.append(c); + break; + } + } + return true; +} + +} + +rtl::Reference< RequestHandler > RequestHandler::pGlobal; + +// Turns a string in aMsg such as file:///home/foo/.libreoffice/3 +// Into a hex string of well known length ff132a86... +static OUString CreateMD5FromString( const OUString& aMsg ) +{ + SAL_INFO("desktop.app", "create md5 from '" << aMsg << "'"); + + rtlDigest handle = rtl_digest_create( rtl_Digest_AlgorithmMD5 ); + if ( handle ) + { + const sal_uInt8* pData = reinterpret_cast<const sal_uInt8*>(aMsg.getStr()); + sal_uInt32 nSize = aMsg.getLength() * sizeof( sal_Unicode ); + sal_uInt32 nMD5KeyLen = rtl_digest_queryLength( handle ); + std::unique_ptr<sal_uInt8[]> pMD5KeyBuffer(new sal_uInt8[ nMD5KeyLen ]); + + rtl_digest_init( handle, pData, nSize ); + rtl_digest_update( handle, pData, nSize ); + rtl_digest_get( handle, pMD5KeyBuffer.get(), nMD5KeyLen ); + rtl_digest_destroy( handle ); + + // Create hex-value string from the MD5 value to keep the string size minimal + OUStringBuffer aBuffer( nMD5KeyLen * 2 + 1 ); + for ( sal_uInt32 i = 0; i < nMD5KeyLen; i++ ) + aBuffer.append( static_cast<sal_Int32>(pMD5KeyBuffer[i]), 16 ); + + return aBuffer.makeStringAndClear(); + } + + return OUString(); +} + +namespace { + +class ProcessEventsClass_Impl +{ +public: + DECL_STATIC_LINK( ProcessEventsClass_Impl, CallEvent, void*, void ); + DECL_STATIC_LINK( ProcessEventsClass_Impl, ProcessDocumentsEvent, void*, void ); +}; + +} + +IMPL_STATIC_LINK( ProcessEventsClass_Impl, CallEvent, void*, pEvent, void ) +{ + // Application events are processed by the Desktop::HandleAppEvent implementation. + Desktop::HandleAppEvent( *static_cast<ApplicationEvent*>(pEvent) ); + delete static_cast<ApplicationEvent*>(pEvent); +} + +IMPL_STATIC_LINK( ProcessEventsClass_Impl, ProcessDocumentsEvent, void*, pEvent, void ) +{ + // Documents requests are processed by the RequestHandler implementation + ProcessDocumentsRequest* pDocsRequest = static_cast<ProcessDocumentsRequest*>(pEvent); + RequestHandler::ExecuteCmdLineRequests(*pDocsRequest, false); + delete pDocsRequest; +} + +static void ImplPostForeignAppEvent( ApplicationEvent* pEvent ) +{ + Application::PostUserEvent( LINK( nullptr, ProcessEventsClass_Impl, CallEvent ), pEvent ); +} + +static void ImplPostProcessDocumentsEvent( std::unique_ptr<ProcessDocumentsRequest> pEvent ) +{ + Application::PostUserEvent( LINK( nullptr, ProcessEventsClass_Impl, ProcessDocumentsEvent ), pEvent.release() ); +} + +oslSignalAction SalMainPipeExchangeSignal_impl(SAL_UNUSED_PARAMETER void* /*pData*/, oslSignalInfo* pInfo) +{ + if( pInfo->Signal == osl_Signal_Terminate ) + RequestHandler::SetDowning(); + return osl_Signal_ActCallNextHdl; +} + + +// The RequestHandlerController implementation is a bookkeeper for all pending requests +// that were created by the RequestHandler. The requests are waiting to be processed by +// our framework loadComponentFromURL function (e.g. open/print request). +// During shutdown the framework is asking RequestHandlerController about pending requests. +// If there are pending requests framework has to stop the shutdown process. It is waiting +// for these requests because framework is not able to handle shutdown and open a document +// concurrently. + + +// XServiceInfo +OUString SAL_CALL RequestHandlerController::getImplementationName() +{ + return "com.sun.star.comp.RequestHandlerController"; +} + +sal_Bool RequestHandlerController::supportsService( + OUString const & ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +Sequence< OUString > SAL_CALL RequestHandlerController::getSupportedServiceNames() +{ + return { }; +} + +// XEventListener +void SAL_CALL RequestHandlerController::disposing( const EventObject& ) +{ +} + +// XTerminateListener +void SAL_CALL RequestHandlerController::queryTermination( const EventObject& ) +{ + // Desktop ask about pending request through our office ipc pipe. We have to + // be sure that no pending request is waiting because framework is not able to + // handle shutdown and open a document concurrently. + + if ( RequestHandler::AreRequestsPending() ) + throw TerminationVetoException(); + RequestHandler::SetDowning(); +} + +void SAL_CALL RequestHandlerController::notifyTermination( const EventObject& ) +{ +} + +class IpcThread: public salhelper::Thread { +public: + void start(RequestHandler * handler) { + m_handler = handler; + launch(); + } + + virtual void close() = 0; + +protected: + explicit IpcThread(char const * name): Thread(name), m_handler(nullptr) {} + + virtual ~IpcThread() override {} + + bool process(OString const & arguments, bool * waitProcessed); + + RequestHandler * m_handler; +}; + +class PipeIpcThread: public IpcThread { +public: + static RequestHandler::Status enable(rtl::Reference<IpcThread> * thread); + +private: + explicit PipeIpcThread(osl::Pipe pipe): + IpcThread("PipeIPC"), pipe_(std::move(pipe)) + {} + + virtual ~PipeIpcThread() override {} + + void execute() override; + + void close() override { pipe_.close(); } + + osl::Pipe pipe_; +}; + +#if ENABLE_DBUS + +namespace { + +struct DbusConnectionHolder { + explicit DbusConnectionHolder(DBusConnection * theConnection): + connection(theConnection) + {} + + DbusConnectionHolder(DbusConnectionHolder && other): connection(nullptr) + { std::swap(connection, other.connection); } + + ~DbusConnectionHolder() { + if (connection != nullptr) { + dbus_connection_close(connection); + dbus_connection_unref(connection); + } + } + + DBusConnection * connection; +}; + +struct DbusMessageHolder { + explicit DbusMessageHolder(DBusMessage * theMessage): message(theMessage) {} + + ~DbusMessageHolder() { clear(); } + + void clear() { + if (message != nullptr) { + dbus_message_unref(message); + } + message = nullptr; + } + + DBusMessage * message; + +private: + DbusMessageHolder(DbusMessageHolder const &) = delete; + DbusMessageHolder& operator =(DbusMessageHolder const &) = delete; +}; + +} + +class DbusIpcThread: public IpcThread { +public: + static RequestHandler::Status enable(rtl::Reference<IpcThread> * thread); + +private: + explicit DbusIpcThread(DbusConnectionHolder && connection): + IpcThread("DbusIPC"), connection_(std::move(connection)) + {} + + virtual ~DbusIpcThread() override {} + + void execute() override; + + void close() override; + + DbusConnectionHolder connection_; +}; + +RequestHandler::Status DbusIpcThread::enable(rtl::Reference<IpcThread> * thread) +{ + assert(thread != nullptr); + if (!dbus_threads_init_default()) { + SAL_WARN("desktop.app", "dbus_threads_init_default failed"); + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + DBusError e; + dbus_error_init(&e); + DbusConnectionHolder con(dbus_bus_get_private(DBUS_BUS_SESSION, &e)); + assert((con.connection == nullptr) == bool(dbus_error_is_set(&e))); + if (con.connection == nullptr) { + SAL_WARN( + "desktop.app", + "dbus_bus_get_private failed with: " << e.name << ": " + << e.message); + dbus_error_free(&e); + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + for (;;) { + int n = dbus_bus_request_name( + con.connection, "org.libreoffice.LibreOfficeIpc0", + DBUS_NAME_FLAG_DO_NOT_QUEUE, &e); + assert((n == -1) == bool(dbus_error_is_set(&e))); + switch (n) { + case -1: + SAL_WARN( + "desktop.app", + "dbus_bus_request_name failed with: " << e.name << ": " + << e.message); + dbus_error_free(&e); + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + case DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER: + *thread = new DbusIpcThread(std::move(con)); + return RequestHandler::IPC_STATUS_OK; + case DBUS_REQUEST_NAME_REPLY_EXISTS: + { + OStringBuffer buf(ARGUMENT_PREFIX); + OUString arg; + if (!(utl::Bootstrap::getProcessWorkingDir(arg) + && addArgument(buf, '1', arg))) + { + buf.append('0'); + } + sal_uInt32 narg = rtl_getAppCommandArgCount(); + for (sal_uInt32 i = 0; i != narg; ++i) { + rtl_getAppCommandArg(i, &arg.pData); + if (!addArgument(buf, ',', arg)) { + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + } + char const * argstr = buf.getStr(); + DbusMessageHolder msg( + dbus_message_new_method_call( + "org.libreoffice.LibreOfficeIpc0", + "/org/libreoffice/LibreOfficeIpc0", + "org.libreoffice.LibreOfficeIpcIfc0", "Execute")); + if (msg.message == nullptr) { + SAL_WARN( + "desktop.app", "dbus_message_new_method_call failed"); + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + DBusMessageIter it; + dbus_message_iter_init_append(msg.message, &it); + if (!dbus_message_iter_append_basic( + &it, DBUS_TYPE_STRING, &argstr)) + { + SAL_WARN( + "desktop.app", "dbus_message_iter_append_basic failed"); + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + DbusMessageHolder repl( + dbus_connection_send_with_reply_and_block( + con.connection, msg.message, 0x7FFFFFFF, &e)); + assert( + (repl.message == nullptr) == bool(dbus_error_is_set(&e))); + if (repl.message == nullptr) { + SAL_INFO( + "desktop.app", + "dbus_connection_send_with_reply_and_block failed" + " with: " << e.name << ": " << e.message); + dbus_error_free(&e); + break; + } + return RequestHandler::IPC_STATUS_2ND_OFFICE; + } + case DBUS_REQUEST_NAME_REPLY_IN_QUEUE: + case DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER: + SAL_WARN( + "desktop.app", + "dbus_bus_request_name failed with unexpected " << +n); + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + default: + for (;;) std::abort(); + } + } +} + +void DbusIpcThread::execute() +{ + assert(m_handler != nullptr); + m_handler->cReady.wait(); + for (;;) { + { + osl::MutexGuard g(RequestHandler::GetMutex()); + if (m_handler->mState == RequestHandler::State::Downing) { + break; + } + } + if (!dbus_connection_read_write(connection_.connection, -1)) { + break; + } + for (;;) { + DbusMessageHolder msg( + dbus_connection_pop_message(connection_.connection)); + if (msg.message == nullptr) { + break; + } + if (!dbus_message_is_method_call( + msg.message, "org.libreoffice.LibreOfficeIpcIfc0", + "Execute")) + { + SAL_INFO("desktop.app", "unknown DBus message ignored"); + continue; + } + DBusMessageIter it; + if (!dbus_message_iter_init(msg.message, &it)) { + SAL_WARN( + "desktop.app", "DBus message without argument ignored"); + continue; + } + if (dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_STRING) { + SAL_WARN( + "desktop.app", + "DBus message with non-string argument ignored"); + continue; + } + char const * argstr; + dbus_message_iter_get_basic(&it, &argstr); + bool waitProcessed = false; + { + osl::MutexGuard g(RequestHandler::GetMutex()); + if (!process(argstr, &waitProcessed)) { + continue; + } + } + if (waitProcessed) { + m_handler->cProcessed.wait(); + } + DbusMessageHolder repl(dbus_message_new_method_return(msg.message)); + if (repl.message == nullptr) { + SAL_WARN( + "desktop.app", "dbus_message_new_method_return failed"); + continue; + } + dbus_uint32_t serial = 0; + if (!dbus_connection_send( + connection_.connection, repl.message, &serial)) { + SAL_WARN("desktop.app", "dbus_connection_send failed"); + continue; + } + dbus_connection_flush(connection_.connection); + } + } +} + +void DbusIpcThread::close() { + assert(connection_.connection != nullptr); + // Make dbus_connection_read_write fall out of internal poll call blocking + // on POLLIN: + int fd; + if (!dbus_connection_get_socket(connection_.connection, &fd)) { + SAL_WARN("desktop.app", "dbus_connection_get_socket failed"); + return; + } + if (shutdown(fd, SHUT_RD) == -1) { + auto const e = errno; + SAL_WARN("desktop.app", "shutdown failed with errno " << e); + } +} + +#endif + +::osl::Mutex& RequestHandler::GetMutex() +{ + static ::osl::Mutex theRequestHandlerMutex; + return theRequestHandlerMutex; +} + +void RequestHandler::SetDowning() +{ + // We have the order to block all incoming requests. Framework + // wants to shutdown and we have to make sure that no loading/printing + // requests are executed anymore. + ::osl::MutexGuard aGuard( GetMutex() ); + + if ( pGlobal.is() ) + pGlobal->mState = State::Downing; +} + +void RequestHandler::EnableRequests() +{ + // switch between just queueing the requests and executing them + ::osl::MutexGuard aGuard( GetMutex() ); + + if ( pGlobal.is() ) + { + if (pGlobal->mState != State::Downing) { + pGlobal->mState = State::RequestsEnabled; + } + ProcessDocumentsRequest aEmptyReq(std::nullopt); + // trigger already queued requests + RequestHandler::ExecuteCmdLineRequests(aEmptyReq, true); + } +} + +bool RequestHandler::AreRequestsPending() +{ + // Give info about pending requests + ::osl::MutexGuard aGuard( GetMutex() ); + if ( pGlobal.is() ) + return ( pGlobal->mnPendingRequests > 0 ); + else + return false; +} + +void RequestHandler::RequestsCompleted() +{ + // Remove nCount pending requests from our internal counter + ::osl::MutexGuard aGuard( GetMutex() ); + if ( pGlobal.is() ) + { + if ( pGlobal->mnPendingRequests > 0 ) + pGlobal->mnPendingRequests --; + } +} + +RequestHandler::Status RequestHandler::Enable(bool ipc) +{ + ::osl::MutexGuard aGuard( GetMutex() ); + + if( pGlobal.is() ) + return IPC_STATUS_OK; + +#if !HAVE_FEATURE_DESKTOP || HAVE_FEATURE_MACOSX_SANDBOX || defined(EMSCRIPTEN) + ipc = false; +#endif + + if (!ipc) { + pGlobal = new RequestHandler; + return IPC_STATUS_OK; + } + + enum class Kind { Pipe, Dbus }; + Kind kind; +#if ENABLE_DBUS + kind = std::getenv("LIBO_FLATPAK") != nullptr ? Kind::Dbus : Kind::Pipe; +#else + kind = Kind::Pipe; +#endif + rtl::Reference<IpcThread> thread; + Status stat = Status(); // silence bogus potentially-uninitialized warnings + switch (kind) { + case Kind::Pipe: + stat = PipeIpcThread::enable(&thread); + break; + case Kind::Dbus: +#if ENABLE_DBUS + stat = DbusIpcThread::enable(&thread); + break; +#endif + default: + assert(false); + } + assert(thread.is() == (stat == IPC_STATUS_OK)); + if (stat == IPC_STATUS_OK) { + pGlobal = new RequestHandler; + pGlobal->mIpcThread = thread; + pGlobal->mIpcThread->start(pGlobal.get()); + } + return stat; +} + +RequestHandler::Status PipeIpcThread::enable(rtl::Reference<IpcThread> * thread) +{ + assert(thread != nullptr); + + // The name of the named pipe is created with the hashcode of the user installation directory (without /user). We have to retrieve + // this information from a unotools implementation. + OUString aUserInstallPath; + ::utl::Bootstrap::PathStatus aLocateResult = ::utl::Bootstrap::locateUserInstallation( aUserInstallPath ); + if (aLocateResult != utl::Bootstrap::PATH_EXISTS + && aLocateResult != utl::Bootstrap::PATH_VALID) + { + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + + // Try to determine if we are the first office or not! This should prevent multiple + // access to the user directory ! + // First we try to create our pipe if this fails we try to connect. We have to do this + // in a loop because the other office can crash or shutdown between createPipe + // and connectPipe!! + auto aUserInstallPathHashCode = CreateMD5FromString(aUserInstallPath); + + // Check result to create a hash code from the user install path + if ( aUserInstallPathHashCode.isEmpty() ) + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; // Something completely broken, we cannot create a valid hash code! + + osl::Pipe pipe; + enum PipeMode + { + PIPEMODE_DONTKNOW, + PIPEMODE_CREATED, + PIPEMODE_CONNECTED + }; + PipeMode nPipeMode = PIPEMODE_DONTKNOW; + + OUString aPipeIdent( "SingleOfficeIPC_" + aUserInstallPathHashCode ); + do + { + osl::Security security; + + // Try to create pipe + if ( pipe.create( aPipeIdent, osl_Pipe_CREATE, security )) + { + // Pipe created + nPipeMode = PIPEMODE_CREATED; + } + else if( pipe.create( aPipeIdent, osl_Pipe_OPEN, security )) // Creation not successful, now we try to connect + { + osl::StreamPipe aStreamPipe(pipe.getHandle()); + if (readStringFromPipe(aStreamPipe) == SEND_ARGUMENTS) + { + // Pipe connected to first office + nPipeMode = PIPEMODE_CONNECTED; + } + else + { + // Pipe connection failed (other office exited or crashed) + TimeValue tval; + tval.Seconds = 0; + tval.Nanosec = 500000000; + salhelper::Thread::wait( tval ); + } + } + else + { + oslPipeError eReason = pipe.getError(); + if ((eReason == osl_Pipe_E_ConnectionRefused) || (eReason == osl_Pipe_E_invalidError)) + return RequestHandler::IPC_STATUS_PIPE_ERROR; + + // Wait for second office to be ready + TimeValue aTimeValue; + aTimeValue.Seconds = 0; + aTimeValue.Nanosec = 10000000; // 10ms + salhelper::Thread::wait( aTimeValue ); + } + + } while ( nPipeMode == PIPEMODE_DONTKNOW ); + + if ( nPipeMode == PIPEMODE_CREATED ) + { + // Seems we are the one and only, so create listening thread + *thread = new PipeIpcThread(pipe); + return RequestHandler::IPC_STATUS_OK; + } + else + { + // Seems another office is running. Pipe arguments to it and self terminate + osl::StreamPipe aStreamPipe(pipe.getHandle()); + + OStringBuffer aArguments(ARGUMENT_PREFIX); + OUString cwdUrl; + if (!(utl::Bootstrap::getProcessWorkingDir(cwdUrl) && + addArgument(aArguments, '1', cwdUrl))) + { + aArguments.append('0'); + } + sal_uInt32 nCount = rtl_getAppCommandArgCount(); + for( sal_uInt32 i=0; i < nCount; i++ ) + { + rtl_getAppCommandArg( i, &aUserInstallPath.pData ); + if (!addArgument(aArguments, ',', aUserInstallPath)) { + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + } + aArguments.append('\0'); + // finally, write the string onto the pipe + SAL_INFO("desktop.app", "writing <" << aArguments.getStr() << ">"); + sal_Int32 n = aStreamPipe.write( + aArguments.getStr(), aArguments.getLength()); + if (n != aArguments.getLength()) { + SAL_INFO("desktop.app", "short write: " << n); + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + + if (readStringFromPipe(aStreamPipe) != PROCESSING_DONE) + { + // something went wrong + return RequestHandler::IPC_STATUS_BOOTSTRAP_ERROR; + } + + return RequestHandler::IPC_STATUS_2ND_OFFICE; + } +} + +void RequestHandler::Disable() +{ + osl::ClearableMutexGuard aMutex( GetMutex() ); + + if( !pGlobal.is() ) + return; + + rtl::Reference< RequestHandler > handler(pGlobal); + pGlobal.clear(); + + handler->mState = State::Downing; + if (handler->mIpcThread.is()) { + handler->mIpcThread->close(); + } + + // release mutex to avoid deadlocks + aMutex.clear(); + + handler->cReady.set(); + + // exit gracefully and join + if (handler->mIpcThread.is()) + { + handler->mIpcThread->join(); + handler->mIpcThread.clear(); + } + + handler->cReady.reset(); +} + +RequestHandler::RequestHandler() : + mState( State::Starting ), + mnPendingRequests( 0 ) +{ +} + +RequestHandler::~RequestHandler() +{ + assert(!mIpcThread.is()); +} + +void RequestHandler::SetReady(bool bIsReady) +{ + osl::MutexGuard g(GetMutex()); + if (pGlobal.is()) + { + if (bIsReady) + pGlobal->cReady.set(); + else + pGlobal->cReady.reset(); + } +} + +void RequestHandler::WaitForReady() +{ + rtl::Reference<RequestHandler> t; + { + osl::MutexGuard g(GetMutex()); + t = pGlobal; + } + if (t.is()) + { + t->cReady.wait(); + } +} + +bool IpcThread::process(OString const & arguments, bool * waitProcessed) { + assert(waitProcessed != nullptr); + + std::unique_ptr< CommandLineArgs > aCmdLineArgs; + try + { + Parser p(arguments); + aCmdLineArgs.reset( new CommandLineArgs( p ) ); + } + catch ( const CommandLineArgs::Supplier::Exception & ) + { + SAL_WARN("desktop.app", "Error in received command line arguments"); + return false; + } + + bool bDocRequestSent = false; + + OUString aUnknown( aCmdLineArgs->GetUnknown() ); + if (aUnknown.isEmpty() && !aCmdLineArgs->IsHelp() && !aCmdLineArgs->IsVersion()) + { + const CommandLineArgs &rCurrentCmdLineArgs = Desktop::GetCommandLineArgs(); + + if ( aCmdLineArgs->IsQuickstart() ) + { + // we have to use application event, because we have to start quickstart service in main thread!! + ApplicationEvent* pAppEvent = + new ApplicationEvent(ApplicationEvent::Type::QuickStart); + ImplPostForeignAppEvent( pAppEvent ); + } + + // handle request for acceptor + std::vector< OUString > const & accept = aCmdLineArgs->GetAccept(); + for (auto const& elem : accept) + { + ApplicationEvent* pAppEvent = new ApplicationEvent( + ApplicationEvent::Type::Accept, elem); + ImplPostForeignAppEvent( pAppEvent ); + } + // handle acceptor removal + std::vector< OUString > const & unaccept = aCmdLineArgs->GetUnaccept(); + for (auto const& elem : unaccept) + { + ApplicationEvent* pAppEvent = new ApplicationEvent( + ApplicationEvent::Type::Unaccept, elem); + ImplPostForeignAppEvent( pAppEvent ); + } + + std::unique_ptr<ProcessDocumentsRequest> pRequest(new ProcessDocumentsRequest( + aCmdLineArgs->getCwdUrl())); + m_handler->cProcessed.reset(); + pRequest->pcProcessed = &m_handler->cProcessed; + m_handler->mbSuccess = false; + pRequest->mpbSuccess = &m_handler->mbSuccess; + + // Print requests are not dependent on the --invisible cmdline argument as they are + // loaded with the "hidden" flag! So they are always checked. + pRequest->aPrintList = aCmdLineArgs->GetPrintList(); + bDocRequestSent |= !pRequest->aPrintList.empty(); + pRequest->aPrintToList = aCmdLineArgs->GetPrintToList(); + pRequest->aPrinterName = aCmdLineArgs->GetPrinterName(); + bDocRequestSent |= !( pRequest->aPrintToList.empty() || pRequest->aPrinterName.isEmpty() ); + pRequest->aConversionList = aCmdLineArgs->GetConversionList(); + pRequest->aConversionParams = aCmdLineArgs->GetConversionParams(); + pRequest->aConversionOut = aCmdLineArgs->GetConversionOut(); + pRequest->aImageConversionType = aCmdLineArgs->GetImageConversionType(); + pRequest->aInFilter = aCmdLineArgs->GetInFilter(); + pRequest->bTextCat = aCmdLineArgs->IsTextCat(); + pRequest->bScriptCat = aCmdLineArgs->IsScriptCat(); + bDocRequestSent |= !pRequest->aConversionList.empty(); + + if ( !rCurrentCmdLineArgs.IsInvisible() ) + { + // Read cmdline args that can open/create documents. As they would open a window + // they are only allowed if the "--invisible" is currently not used! + pRequest->aOpenList = aCmdLineArgs->GetOpenList(); + bDocRequestSent |= !pRequest->aOpenList.empty(); + pRequest->aViewList = aCmdLineArgs->GetViewList(); + bDocRequestSent |= !pRequest->aViewList.empty(); + pRequest->aStartList = aCmdLineArgs->GetStartList(); + bDocRequestSent |= !pRequest->aStartList.empty(); + pRequest->aForceOpenList = aCmdLineArgs->GetForceOpenList(); + bDocRequestSent |= !pRequest->aForceOpenList.empty(); + pRequest->aForceNewList = aCmdLineArgs->GetForceNewList(); + bDocRequestSent |= !pRequest->aForceNewList.empty(); + + // Special command line args to create an empty document for a given module + + // #i18338# (lo) + // we only do this if no document was specified on the command line, + // since this would be inconsistent with the behaviour of + // the first process, see OpenClients() (call to OpenDefault()) in app.cxx + if ( aCmdLineArgs->HasModuleParam() && !bDocRequestSent ) + { + SvtModuleOptions aOpt; + SvtModuleOptions::EFactory eFactory = SvtModuleOptions::EFactory::WRITER; + if ( aCmdLineArgs->IsWriter() ) + eFactory = SvtModuleOptions::EFactory::WRITER; + else if ( aCmdLineArgs->IsCalc() ) + eFactory = SvtModuleOptions::EFactory::CALC; + else if ( aCmdLineArgs->IsDraw() ) + eFactory = SvtModuleOptions::EFactory::DRAW; + else if ( aCmdLineArgs->IsImpress() ) + eFactory = SvtModuleOptions::EFactory::IMPRESS; + else if ( aCmdLineArgs->IsBase() ) + eFactory = SvtModuleOptions::EFactory::DATABASE; + else if ( aCmdLineArgs->IsMath() ) + eFactory = SvtModuleOptions::EFactory::MATH; + else if ( aCmdLineArgs->IsGlobal() ) + eFactory = SvtModuleOptions::EFactory::WRITERGLOBAL; + else if ( aCmdLineArgs->IsWeb() ) + eFactory = SvtModuleOptions::EFactory::WRITERWEB; + + if ( !pRequest->aOpenList.empty() ) + pRequest->aModule = aOpt.GetFactoryName( eFactory ); + else + pRequest->aOpenList.push_back( aOpt.GetFactoryEmptyDocumentURL( eFactory ) ); + bDocRequestSent = true; + } + } + + if ( !aCmdLineArgs->IsQuickstart() ) { + bool bShowHelp = false; + OUStringBuffer aHelpURLBuffer; + if (aCmdLineArgs->IsHelpWriter()) { + bShowHelp = true; + aHelpURLBuffer.append("vnd.sun.star.help://swriter/start"); + } else if (aCmdLineArgs->IsHelpCalc()) { + bShowHelp = true; + aHelpURLBuffer.append("vnd.sun.star.help://scalc/start"); + } else if (aCmdLineArgs->IsHelpDraw()) { + bShowHelp = true; + aHelpURLBuffer.append("vnd.sun.star.help://sdraw/start"); + } else if (aCmdLineArgs->IsHelpImpress()) { + bShowHelp = true; + aHelpURLBuffer.append("vnd.sun.star.help://simpress/start"); + } else if (aCmdLineArgs->IsHelpBase()) { + bShowHelp = true; + aHelpURLBuffer.append("vnd.sun.star.help://sdatabase/start"); + } else if (aCmdLineArgs->IsHelpBasic()) { + bShowHelp = true; + aHelpURLBuffer.append("vnd.sun.star.help://sbasic/start"); + } else if (aCmdLineArgs->IsHelpMath()) { + bShowHelp = true; + aHelpURLBuffer.append("vnd.sun.star.help://smath/start"); + } + if (bShowHelp) { + aHelpURLBuffer.append("?Language="); + aHelpURLBuffer.append(utl::ConfigManager::getUILocale()); +#if defined UNX + aHelpURLBuffer.append("&System=UNX"); +#elif defined _WIN32 + aHelpURLBuffer.append("&System=WIN"); +#endif + ApplicationEvent* pAppEvent = new ApplicationEvent( + ApplicationEvent::Type::OpenHelpUrl, + aHelpURLBuffer.makeStringAndClear()); + ImplPostForeignAppEvent( pAppEvent ); + } + } + + if ( bDocRequestSent ) + { + // Send requests to dispatch watcher if we have at least one. The receiver + // is responsible to delete the request after processing it. + if ( aCmdLineArgs->HasModuleParam() ) + { + SvtModuleOptions aOpt; + + // Support command line parameters to start a module (as preselection) + if ( aCmdLineArgs->IsWriter() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::WRITER ) ) + pRequest->aModule = aOpt.GetFactoryName( SvtModuleOptions::EFactory::WRITER ); + else if ( aCmdLineArgs->IsCalc() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::CALC ) ) + pRequest->aModule = aOpt.GetFactoryName( SvtModuleOptions::EFactory::CALC ); + else if ( aCmdLineArgs->IsImpress() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::IMPRESS ) ) + pRequest->aModule= aOpt.GetFactoryName( SvtModuleOptions::EFactory::IMPRESS ); + else if ( aCmdLineArgs->IsDraw() && aOpt.IsModuleInstalled( SvtModuleOptions::EModule::DRAW ) ) + pRequest->aModule= aOpt.GetFactoryName( SvtModuleOptions::EFactory::DRAW ); + } + + ImplPostProcessDocumentsEvent( std::move(pRequest) ); + } + else + { + // delete not used request again + pRequest.reset(); + } + if (aCmdLineArgs->IsEmpty()) + { + // no document was sent, just bring Office to front + ApplicationEvent* pAppEvent = + new ApplicationEvent(ApplicationEvent::Type::Appear); + ImplPostForeignAppEvent( pAppEvent ); + } + } + *waitProcessed = bDocRequestSent; + return true; +} + +void PipeIpcThread::execute() +{ + assert(m_handler != nullptr); + do + { + osl::StreamPipe aStreamPipe; + oslPipeError nError = pipe_.accept( aStreamPipe ); + + + if( nError == osl_Pipe_E_None ) + { + // if we receive a request while the office is displaying some dialog or error during + // bootstrap, that dialogs event loop might get events that are dispatched by this thread + // we have to wait for cReady to be set by the real main loop. + // only requests that don't dispatch events may be processed before cReady is set. + m_handler->cReady.wait(); + + // we might have decided to shutdown while we were sleeping + if (!RequestHandler::pGlobal.is()) return; + + // only lock the mutex when processing starts, otherwise we deadlock when the office goes + // down during wait + osl::ClearableMutexGuard aGuard( RequestHandler::GetMutex() ); + + if (m_handler->mState == RequestHandler::State::Downing) + { + break; + } + + // notify client we're ready to process its args: + SAL_INFO("desktop.app", "writing <" << SEND_ARGUMENTS << ">"); + sal_Int32 n = aStreamPipe.write( + SEND_ARGUMENTS, SAL_N_ELEMENTS(SEND_ARGUMENTS)); + // incl. terminating NUL + if (n != SAL_N_ELEMENTS(SEND_ARGUMENTS)) { + SAL_WARN("desktop.app", "short write: " << n); + continue; + } + + OString aArguments = readStringFromPipe(aStreamPipe); + + // Is this a lookup message from another application? if so, ignore + if (aArguments.isEmpty()) + continue; + + bool waitProcessed = false; + if (!process(aArguments, &waitProcessed)) { + continue; + } + + // we don't need the mutex any longer... + aGuard.clear(); + bool bSuccess = true; + // wait for processing to finish + if (waitProcessed) + { + m_handler->cProcessed.wait(); + bSuccess = m_handler->mbSuccess; + } + if (bSuccess) + { + // processing finished, inform the requesting end: + SAL_INFO("desktop.app", "writing <" << PROCESSING_DONE << ">"); + n = aStreamPipe.write(PROCESSING_DONE, SAL_N_ELEMENTS(PROCESSING_DONE)); + // incl. terminating NUL + if (n != SAL_N_ELEMENTS(PROCESSING_DONE)) + { + SAL_WARN("desktop.app", "short write: " << n); + continue; + } + } + } + else + { + { + osl::MutexGuard aGuard( RequestHandler::GetMutex() ); + if (m_handler->mState == RequestHandler::State::Downing) + { + break; + } + } + + SAL_WARN( "desktop.app", "Error on accept: " << static_cast<int>(nError)); + TimeValue tval; + tval.Seconds = 1; + tval.Nanosec = 0; + salhelper::Thread::wait( tval ); + } + } while( schedule() ); +} + +static void AddToDispatchList( + std::vector<DispatchWatcher::DispatchRequest>& rDispatchList, + std::optional< OUString > const & cwdUrl, + std::vector< OUString > const & aRequestList, + DispatchWatcher::RequestType nType, + const OUString& aParam, + const OUString& aFactory ) +{ + for (auto const& request : aRequestList) + { + rDispatchList.push_back({nType, request, cwdUrl, aParam, aFactory}); + } +} + +static void AddConversionsToDispatchList( + std::vector<DispatchWatcher::DispatchRequest>& rDispatchList, + std::optional< OUString > const & cwdUrl, + std::vector< OUString > const & rRequestList, + const OUString& rParam, + const OUString& rPrinterName, + const OUString& rFactory, + const OUString& rParamOut, + std::u16string_view rImgOut, + const bool isTextCat, + const bool isScriptCat ) +{ + DispatchWatcher::RequestType nType; + OUString aParam( rParam ); + + if( !rParam.isEmpty() ) + { + if ( isTextCat ) + nType = DispatchWatcher::REQUEST_CAT; + else + nType = DispatchWatcher::REQUEST_CONVERSION; + aParam = rParam; + } + else + { + if ( isScriptCat ) + nType = DispatchWatcher::REQUEST_SCRIPT_CAT; + else + { + nType = DispatchWatcher::REQUEST_BATCHPRINT; + aParam = rPrinterName; + } + } + + OUString aOutDir( rParamOut.trim() ); + std::u16string_view aImgOut = o3tl::trim(rImgOut); + OUString aPWD; + if (cwdUrl) + { + aPWD = *cwdUrl; + } + else + { + utl::Bootstrap::getProcessWorkingDir( aPWD ); + } + + if( !::osl::FileBase::getAbsoluteFileURL( aPWD, rParamOut, aOutDir ) ) + ::osl::FileBase::getSystemPathFromFileURL( aOutDir, aOutDir ); + + if( !rParamOut.trim().isEmpty() ) + { + aParam += ";" + aOutDir; + } + else + { + ::osl::FileBase::getSystemPathFromFileURL( aPWD, aPWD ); + aParam += ";" + aPWD; + } + + if( !rImgOut.empty() ) + aParam += OUString::Concat("|") + aImgOut; + + for (auto const& request : rRequestList) + { + rDispatchList.push_back({nType, request, cwdUrl, aParam, rFactory}); + } +} + +namespace { + +struct ConditionSetGuard +{ + osl::Condition* m_pCondition; + ConditionSetGuard(osl::Condition* pCondition) : m_pCondition(pCondition) {} + ~ConditionSetGuard() { if (m_pCondition) m_pCondition->set(); } +}; + +} + +bool RequestHandler::ExecuteCmdLineRequests( + ProcessDocumentsRequest& aRequest, bool noTerminate) +{ + // protect the dispatch list + osl::ClearableMutexGuard aGuard( GetMutex() ); + + // ensure that Processed flag (if exists) is signaled in any outcome + ConditionSetGuard aSetGuard(aRequest.pcProcessed); + + static std::vector<DispatchWatcher::DispatchRequest> aDispatchList; + + // Create dispatch list for dispatch watcher + AddToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aInFilter, DispatchWatcher::REQUEST_INFILTER, "", aRequest.aModule ); + AddToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aOpenList, DispatchWatcher::REQUEST_OPEN, "", aRequest.aModule ); + AddToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aViewList, DispatchWatcher::REQUEST_VIEW, "", aRequest.aModule ); + AddToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aStartList, DispatchWatcher::REQUEST_START, "", aRequest.aModule ); + AddToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aPrintList, DispatchWatcher::REQUEST_PRINT, "", aRequest.aModule ); + AddToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aPrintToList, DispatchWatcher::REQUEST_PRINTTO, aRequest.aPrinterName, aRequest.aModule ); + AddToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aForceOpenList, DispatchWatcher::REQUEST_FORCEOPEN, "", aRequest.aModule ); + AddToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aForceNewList, DispatchWatcher::REQUEST_FORCENEW, "", aRequest.aModule ); + AddConversionsToDispatchList( aDispatchList, aRequest.aCwdUrl, aRequest.aConversionList, aRequest.aConversionParams, aRequest.aPrinterName, aRequest.aModule, aRequest.aConversionOut, aRequest.aImageConversionType, aRequest.bTextCat, aRequest.bScriptCat ); + bool bShutdown( false ); + + if ( pGlobal.is() ) + { + if( ! pGlobal->AreRequestsEnabled() ) + { + // Either starting, or downing - do not process the request, just try to bring Office to front + ApplicationEvent* pAppEvent = + new ApplicationEvent(ApplicationEvent::Type::Appear); + ImplPostForeignAppEvent(pAppEvent); + return bShutdown; + } + + pGlobal->mnPendingRequests += aDispatchList.size(); + if ( !pGlobal->mpDispatchWatcher.is() ) + { + pGlobal->mpDispatchWatcher = new DispatchWatcher; + } + rtl::Reference<DispatchWatcher> dispatchWatcher( + pGlobal->mpDispatchWatcher); + + // copy for execute + std::vector<DispatchWatcher::DispatchRequest> aTempList; + aTempList.swap( aDispatchList ); + + aGuard.clear(); + + // Execute dispatch requests + bShutdown = dispatchWatcher->executeDispatchRequests( aTempList, noTerminate); + if (aRequest.mpbSuccess) + *aRequest.mpbSuccess = true; // signal that we have actually succeeded + } + + return bShutdown; +} + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/officeipcthread.hxx b/desktop/source/app/officeipcthread.hxx new file mode 100644 index 000000000..a233c18e0 --- /dev/null +++ b/desktop/source/app/officeipcthread.hxx @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <sal/config.h> + +#include <utility> +#include <vector> + +#include <com/sun/star/lang/XServiceInfo.hpp> +#include <com/sun/star/frame/XTerminateListener.hpp> +#include <osl/signal.h> +#include <rtl/ref.hxx> +#include <rtl/ustring.hxx> +#include <salhelper/simplereferenceobject.hxx> +#include <cppuhelper/implbase.hxx> +#include <osl/conditn.hxx> +#include <optional> + +namespace desktop +{ + +oslSignalAction SalMainPipeExchangeSignal_impl(void* /*pData*/, oslSignalInfo* pInfo); + +// A request for the current office +// that was given by command line or by IPC pipe communication. +struct ProcessDocumentsRequest +{ + explicit ProcessDocumentsRequest(std::optional< OUString > cwdUrl): + aCwdUrl(std::move(cwdUrl)), pcProcessed( nullptr ), bTextCat( false ), bScriptCat( false ) {} + + std::optional< OUString > aCwdUrl; + OUString aModule; + std::vector< OUString > aOpenList; // Documents that should be opened in the default way + std::vector< OUString > aViewList; // Documents that should be opened in viewmode + std::vector< OUString > aStartList; // Documents/Presentations that should be started + std::vector< OUString > aPrintList; // Documents that should be printed on default printer + std::vector< OUString > aForceOpenList; // Documents that should be forced to open for editing (even templates) + std::vector< OUString > aForceNewList; // Documents that should be forced to create a new document + OUString aPrinterName; // The printer name that should be used for printing + std::vector< OUString > aPrintToList; // Documents that should be printed on the given printer + std::vector< OUString > aConversionList; + OUString aConversionParams; + OUString aConversionOut; + OUString aImageConversionType; + std::vector< OUString > aInFilter; + ::osl::Condition *pcProcessed; // pointer condition to be set when the request has been processed + bool* mpbSuccess = nullptr; // pointer to boolean receiving if the processing was successful + bool bTextCat; // boolean flag indicating whether to dump text content to console + bool bScriptCat; // boolean flag indicating whether to dump script content to console +}; + +class DispatchWatcher; +class IpcThread; +class PipeIpcThread; +class DbusIpcThread; + +class RequestHandler: public salhelper::SimpleReferenceObject +{ + friend IpcThread; + friend PipeIpcThread; + friend DbusIpcThread; + + private: + static rtl::Reference< RequestHandler > pGlobal; + + enum class State { Starting, RequestsEnabled, Downing }; + + State mState; + int mnPendingRequests; + rtl::Reference<DispatchWatcher> mpDispatchWatcher; + rtl::Reference<IpcThread> mIpcThread; + + /* condition to be set when the request has been processed */ + ::osl::Condition cProcessed; + /* receives if the processing was successful (may be false e.g. when shutting down) */ + bool mbSuccess = false; + + /* condition to be set when the main event loop is ready + otherwise an error dialogs event loop could eat away + requests from a 2nd office */ + ::osl::Condition cReady; + + static ::osl::Mutex& GetMutex(); + + RequestHandler(); + + virtual ~RequestHandler() override; + + public: + enum Status + { + IPC_STATUS_OK, + IPC_STATUS_2ND_OFFICE, + IPC_STATUS_PIPE_ERROR, + IPC_STATUS_BOOTSTRAP_ERROR + }; + + // controlling pipe communication during shutdown + static void SetDowning(); + static void EnableRequests(); + static bool AreRequestsPending(); + static void RequestsCompleted(); + static bool ExecuteCmdLineRequests( + ProcessDocumentsRequest&, bool noTerminate); + + // return sal_False if second office + static Status Enable(bool ipc); + static void Disable(); + // start dispatching events... + static void SetReady(bool bIsReady); + static void WaitForReady(); + + bool AreRequestsEnabled() const { return mState == State::RequestsEnabled; } +}; + + +class RequestHandlerController : public ::cppu::WeakImplHelper< + css::lang::XServiceInfo, + css::frame::XTerminateListener > +{ + public: + RequestHandlerController() {} + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService( const OUString& ServiceName ) override; + virtual css::uno::Sequence< OUString > SAL_CALL getSupportedServiceNames() override; + + // XEventListener + virtual void SAL_CALL disposing( const css::lang::EventObject& Source ) override; + + // XTerminateListener + virtual void SAL_CALL queryTermination( const css::lang::EventObject& aEvent ) override; + virtual void SAL_CALL notifyTermination( const css::lang::EventObject& aEvent ) override; +}; + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/opencl.cxx b/desktop/source/app/opencl.cxx new file mode 100644 index 000000000..fb5c3723e --- /dev/null +++ b/desktop/source/app/opencl.cxx @@ -0,0 +1,257 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + */ +/* + * This module exists to validate the OpenCL implementation, + * where necessary during startup; and before we load or + * calculate using OpenCL. + */ + +#include <app.hxx> + +#include <config_version.h> +#include <config_feature_opencl.h> +#include <config_folders.h> + +#include <rtl/bootstrap.hxx> +#include <sal/log.hxx> + +#include <officecfg/Office/Calc.hxx> +#include <officecfg/Office/Common.hxx> + +#include <comphelper/propertyvalue.hxx> +#include <svl/documentlockfile.hxx> +#include <tools/diagnose_ex.h> + +#include <com/sun/star/table/XCell2.hpp> +#include <com/sun/star/sheet/XCalculatable.hpp> +#include <com/sun/star/sheet/XSpreadsheet.hpp> +#include <com/sun/star/sheet/XSpreadsheets.hpp> +#include <com/sun/star/sheet/XSpreadsheetDocument.hpp> + +#if HAVE_FEATURE_OPENCL +#include <opencl/openclwrapper.hxx> +#endif +#include <opencl/OpenCLZone.hxx> + +#include <osl/file.hxx> +#include <osl/process.h> + +using namespace ::osl; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::frame; + +namespace desktop { + +#if HAVE_FEATURE_OPENCL + +static bool testOpenCLDriver() +{ + // A simple OpenCL test run in a separate process in order to test + // whether the driver crashes (asserts,etc.) when trying to use OpenCL. + SAL_INFO("opencl", "Starting CL driver test"); + + OUString testerURL("$BRAND_BASE_DIR/" LIBO_BIN_FOLDER "/opencltest"); + rtl::Bootstrap::expandMacros(testerURL); //TODO: detect failure + + OUString deviceName, platformName; + openclwrapper::getOpenCLDeviceName( deviceName, platformName ); + rtl_uString* args[] = { deviceName.pData, platformName.pData }; + sal_Int32 numArgs = 2; + + oslProcess process; + oslSecurity security = osl_getCurrentSecurity(); + oslProcessError error = osl_executeProcess(testerURL.pData, args, numArgs, + osl_Process_SEARCHPATH | osl_Process_HIDDEN, security, + nullptr, nullptr, 0, &process ); + osl_freeSecurityHandle( security ); + if( error != osl_Process_E_None ) + { + SAL_WARN( "opencl", "failed to start CL driver test: " << error ); + return false; + } + // If the driver takes more than 10 seconds, it's probably broken/useless. + TimeValue timeout( 10, 0 ); + error = osl_joinProcessWithTimeout( process, &timeout ); + if( error == osl_Process_E_None ) + { + oslProcessInfo info; + info.Size = sizeof( info ); + error = osl_getProcessInfo( process, osl_Process_EXITCODE, &info ); + if( error == osl_Process_E_None ) + { + if( info.Code == 0 ) + { + SAL_INFO( "opencl", "CL driver test passed" ); + osl_freeProcessHandle( process ); + return true; + } + else + { + SAL_WARN( "opencl", "CL driver test failed - disabling: " << info.Code ); + osl_freeProcessHandle( process ); + return false; + } + } + } + SAL_WARN( "opencl", "CL driver test did not finish - disabling: " << error ); + osl_terminateProcess( process ); + osl_freeProcessHandle( process ); + return false; +} + +static bool testOpenCLCompute(const Reference< XDesktop2 > &xDesktop, const OUString &rURL) +{ + bool bSuccess = false; + css::uno::Reference< css::lang::XComponent > xComponent; + + sal_uInt64 nKernelFailures = openclwrapper::kernelFailures; + + SAL_INFO("opencl", "Starting CL test spreadsheet"); + + // A stale lock file would make the loading fail, so make sure to remove it. + try { + ::svt::DocumentLockFile lockFile( rURL ); + lockFile.RemoveFileDirectly(); + } + catch (const css::uno::Exception&) + { + } + + try { + css::uno::Reference< css::frame::XComponentLoader > xLoader(xDesktop, css::uno::UNO_QUERY_THROW); + + css::uno::Sequence< css::beans::PropertyValue > aArgs{ comphelper::makePropertyValue("Hidden", + true) }; + + xComponent.set(xLoader->loadComponentFromURL(rURL, "_blank", 0, aArgs)); + + // What an unpleasant API to use. + css::uno::Reference< css::sheet::XCalculatable > xCalculatable( xComponent, css::uno::UNO_QUERY_THROW); + css::uno::Reference< css::sheet::XSpreadsheetDocument > xSpreadDoc( xComponent, css::uno::UNO_QUERY_THROW ); + css::uno::Reference< css::sheet::XSpreadsheets > xSheets( xSpreadDoc->getSheets(), css::uno::UNO_SET_THROW ); + css::uno::Reference< css::container::XIndexAccess > xIndex( xSheets, css::uno::UNO_QUERY_THROW ); + css::uno::Reference< css::sheet::XSpreadsheet > xSheet( xIndex->getByIndex(0), css::uno::UNO_QUERY_THROW); + + // So we insert our MAX call at the end on a named range. + css::uno::Reference< css::table::XCell2 > xThresh( xSheet->getCellByPosition(1,1), css::uno::UNO_QUERY_THROW ); // B2 + double fThreshold = xThresh->getValue(); + + // We need pure OCL formulae all the way through the + // dependency chain, or we fall-back. + xCalculatable->calculateAll(); + + // So we insert our MAX call at the end on a named range. + css::uno::Reference< css::table::XCell2 > xCell( xSheet->getCellByPosition(1,0), css::uno::UNO_QUERY_THROW ); + xCell->setFormula("=MAX(results)"); + double fResult = xCell->getValue(); + + // Ensure the maximum variance is below our tolerance. + if (fResult > fThreshold) + { + SAL_WARN("opencl", "OpenCL results unstable - disabling; result: " + << fResult << " vs. " << fThreshold); + } + else + { + SAL_INFO("opencl", "calculating smoothly; result: " << fResult); + bSuccess = true; + } + } + catch (const css::uno::Exception &) + { + TOOLS_WARN_EXCEPTION("opencl", "OpenCL testing failed - disabling"); + } + + if (nKernelFailures != openclwrapper::kernelFailures) + { + // tdf#100883 - defeat SEH exception handling fallbacks. + SAL_WARN("opencl", "OpenCL kernels failed to compile, " + "or took SEH exceptions " + << nKernelFailures << " != " << openclwrapper::kernelFailures); + bSuccess = false; + } + + if (!bSuccess) + OpenCLZone::hardDisable(); + if (xComponent.is()) + xComponent->dispose(); + + + return bSuccess; +} + +void Desktop::CheckOpenCLCompute(const Reference< XDesktop2 > &xDesktop) +{ + if (!openclwrapper::canUseOpenCL() || Application::IsSafeModeEnabled()) + return; + + SAL_INFO("opencl", "Initiating test of OpenCL device"); + OpenCLZone aZone; + OpenCLInitialZone aInitialZone; + + OUString aDevice = officecfg::Office::Calc::Formula::Calculation::OpenCLDevice::get(); + OUString aSelectedCLDeviceVersionID; + if (!openclwrapper::switchOpenCLDevice( + &aDevice, + officecfg::Office::Calc::Formula::Calculation::OpenCLAutoSelect::get(), + false /* bForceEvaluation */, + aSelectedCLDeviceVersionID)) + { + SAL_WARN("opencl", "Failed to initialize OpenCL for test"); + OpenCLZone::hardDisable(); + return; + } + + // Append our app version as well. + aSelectedCLDeviceVersionID += "--" LIBO_VERSION_DOTTED; + + // Append timestamp of the file. + OUString aURL("$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/opencl/cl-test.ods"); + rtl::Bootstrap::expandMacros(aURL); + + DirectoryItem aItem; + (void)DirectoryItem::get( aURL, aItem ); + FileStatus aFileStatus( osl_FileStatus_Mask_ModifyTime ); + (void)aItem.getFileStatus( aFileStatus ); + TimeValue aTimeVal = aFileStatus.getModifyTime(); + aSelectedCLDeviceVersionID += "--" + + OUString::number(aTimeVal.Seconds); + + if (aSelectedCLDeviceVersionID == officecfg::Office::Common::Misc::SelectedOpenCLDeviceIdentifier::get()) + return; + + // OpenCL device changed - sanity check it and disable if bad. + + sal_Int32 nOrigMinimumSize = officecfg::Office::Calc::Formula::Calculation::OpenCLMinimumDataSize::get(); + { // set the minimum group size to something small for quick testing. + std::shared_ptr<comphelper::ConfigurationChanges> xBatch(comphelper::ConfigurationChanges::create()); + officecfg::Office::Calc::Formula::Calculation::OpenCLMinimumDataSize::set(3 /* small */, xBatch); + xBatch->commit(); + } + + // Hopefully at least basic functionality always works and broken OpenCL implementations break + // only when they are used to compute something. If this assumptions turns out to be not true, + // the driver check needs to be moved sooner. + bool bSucceeded = testOpenCLDriver() && testOpenCLCompute(xDesktop, aURL); + + { // restore the minimum group size + std::shared_ptr<comphelper::ConfigurationChanges> xBatch(comphelper::ConfigurationChanges::create()); + officecfg::Office::Calc::Formula::Calculation::OpenCLMinimumDataSize::set(nOrigMinimumSize, xBatch); + officecfg::Office::Common::Misc::SelectedOpenCLDeviceIdentifier::set(aSelectedCLDeviceVersionID, xBatch); + xBatch->commit(); + } + + if (!bSucceeded) + OpenCLZone::hardDisable(); +} +#endif // HAVE_FEATURE_OPENCL + +} // end namespace desktop + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/desktop/source/app/sofficemain.cxx b/desktop/source/app/sofficemain.cxx new file mode 100644 index 000000000..0b02155f5 --- /dev/null +++ b/desktop/source/app/sofficemain.cxx @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> +#include <config_features.h> + +#include <desktop/dllapi.h> + +#include <app.hxx> +#include "cmdlineargs.hxx" +#include "cmdlinehelp.hxx" + +// needed before sal/main.h to avoid redefinition of macros +#include <prewin.h> + +#include <rtl/bootstrap.hxx> +#include <sal/log.hxx> +#include <sal/main.h> +#include <tools/extendapplicationenvironment.hxx> +#include <vcl/svmain.hxx> + +#if HAVE_FEATURE_BREAKPAD +#include <desktop/crashreport.hxx> +#endif + +#include <postwin.h> + +#ifdef ANDROID +# include <jni.h> +# include <android/log.h> +# include <salhelper/thread.hxx> + +# define LOGTAG "LibreOffice/sofficemain" +# define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOGTAG, __VA_ARGS__)) +#endif + +extern "C" int DESKTOP_DLLPUBLIC soffice_main() +{ + sal_detail_initialize(sal::detail::InitializeSoffice, nullptr); + +#if HAVE_FEATURE_BREAKPAD + CrashReporter::installExceptionHandler(); +#endif + +#if defined ANDROID + try { + rtl::Bootstrap::setIniFilename("file:///assets/program/lofficerc"); +#endif + tools::extendApplicationEnvironment(); + + desktop::Desktop aDesktop; + // This string is used during initialization of the Gtk+ VCL module + Application::SetAppName( "soffice" ); + + // handle --version and --help already here, otherwise they would be handled + // after VCL initialization that might fail if $DISPLAY is not set + const desktop::CommandLineArgs& rCmdLineArgs = desktop::Desktop::GetCommandLineArgs(); + const OUString& aUnknown( rCmdLineArgs.GetUnknown() ); + if ( !aUnknown.isEmpty() ) + { + desktop::Desktop::InitApplicationServiceManager(); + desktop::displayCmdlineHelp( aUnknown ); + return EXIT_FAILURE; + } + if ( rCmdLineArgs.IsHelp() ) + { + desktop::Desktop::InitApplicationServiceManager(); + desktop::displayCmdlineHelp( OUString() ); + return EXIT_SUCCESS; + } + if ( rCmdLineArgs.IsVersion() ) + { + desktop::Desktop::InitApplicationServiceManager(); + desktop::displayVersion(); + return EXIT_SUCCESS; + } + + return SVMain(); +#if defined ANDROID + } catch (const css::uno::Exception &e) { + LOGI("Unhandled UNO exception: '%s'", + OUStringToOString(e.Message, RTL_TEXTENCODING_UTF8).getStr()); + throw; // to get exception type printed + } +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/sofficemain.h b/desktop/source/app/sofficemain.h new file mode 100644 index 000000000..9bc6dafee --- /dev/null +++ b/desktop/source/app/sofficemain.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <desktop/dllapi.h> + +#if defined __cplusplus +extern "C" { +#endif + +int DESKTOP_DLLPUBLIC soffice_main(void); + +#if defined __cplusplus +} +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/updater.cxx b/desktop/source/app/updater.cxx new file mode 100644 index 000000000..b4be0f7bc --- /dev/null +++ b/desktop/source/app/updater.cxx @@ -0,0 +1,894 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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 "updater.hxx" + +#if UNX +#include <unistd.h> +#include <errno.h> + +#endif + +#ifdef _WIN32 +#include <comphelper/windowsStart.hxx> +#endif + +#include <fstream> +#include <config_folders.h> +#include <rtl/bootstrap.hxx> + +#include <officecfg/Office/Update.hxx> + +#include <rtl/ustring.hxx> +#include <unotools/tempfile.hxx> +#include <unotools/configmgr.hxx> +#include <osl/file.hxx> +#include <rtl/process.h> +#include <sal/log.hxx> + +#include <curl/curl.h> + +#include <orcus/json_document_tree.hpp> +#include <orcus/config.hpp> +#include <orcus/pstring.hpp> +#include <comphelper/hash.hxx> + +#include <com/sun/star/container/XNameAccess.hpp> + +#include <officecfg/Setup.hxx> + +#include <functional> +#include <memory> +#include <set> + +namespace { + +class error_updater : public std::exception +{ + OString maStr; +public: + + error_updater(const OString& rStr): + maStr(rStr) + { + } + + virtual const char* what() const throw() override + { + return maStr.getStr(); + } +}; + +#ifdef UNX +static const char kUserAgent[] = "LibreOffice UpdateChecker/1.0 (Linux)"; +#else +static const char kUserAgent[] = "LibreOffice UpdateChecker/1.0 (unknown platform)"; +#endif + +#ifdef UNX +const char* const pUpdaterName = "updater"; +const char* const pSofficeExeName = "soffice"; +#elif defined(_WIN32) +const char* pUpdaterName = "updater.exe"; +const char* pSofficeExeName = "soffice.exe"; +#else +#error "Need implementation" +#endif + +OUString normalizePath(const OUString& rPath) +{ + OUString aPath = rPath.replaceAll("//", "/"); + + // remove final / + if (aPath.endsWith("/")) + { + aPath = aPath.copy(0, aPath.getLength() - 1); + } + + while (aPath.indexOf("/..") != -1) + { + sal_Int32 nIndex = aPath.indexOf("/.."); + sal_Int32 i = nIndex - 1; + for (; i > 0; --i) + { + if (aPath[i] == '/') + break; + } + + OUString aTempPath = aPath; + aPath = aTempPath.copy(0, i) + aPath.copy(nIndex + 3); + } + + return aPath.replaceAll("\\", "/"); +} + +void CopyFileToDir(const OUString& rTempDirURL, const OUString & rFileName, const OUString& rOldDir) +{ + OUString aSourceURL = rOldDir + "/" + rFileName; + OUString aDestURL = rTempDirURL + "/" + rFileName; + + osl::File::RC eError = osl::File::copy(aSourceURL, aDestURL); + if (eError != osl::File::E_None) + { + SAL_WARN("desktop.updater", "could not copy the file to a temp directory: " << rFileName); + throw std::exception(); + } +} + +OUString getPathFromURL(const OUString& rURL) +{ + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(rURL, aPath); + + return normalizePath(aPath); +} + +void CopyUpdaterToTempDir(const OUString& rInstallDirURL, const OUString& rTempDirURL) +{ + OUString aUpdaterName = OUString::fromUtf8(pUpdaterName); + CopyFileToDir(rTempDirURL, aUpdaterName, rInstallDirURL); +} + +#ifdef UNX +typedef char CharT; +#define tstrncpy std::strncpy +#elif defined(_WIN32) +typedef wchar_t CharT; +#define tstrncpy std::wcsncpy +#else +#error "Need an implementation" +#endif + +void createStr(const OUString& rStr, CharT** pArgs, size_t i) +{ +#ifdef UNX + OString aStr = OUStringToOString(rStr, RTL_TEXTENCODING_UTF8); +#elif defined(_WIN32) + OUString aStr = rStr; +#else +#error "Need an implementation" +#endif + CharT* pStr = new CharT[aStr.getLength() + 1]; + tstrncpy(pStr, (CharT*)aStr.getStr(), aStr.getLength()); + pStr[aStr.getLength()] = '\0'; + pArgs[i] = pStr; +} + +CharT** createCommandLine() +{ + OUString aInstallDir = Updater::getInstallationPath(); + + size_t nCommandLineArgs = rtl_getAppCommandArgCount(); + size_t nArgs = 8 + nCommandLineArgs; + CharT** pArgs = new CharT*[nArgs]; + { + OUString aUpdaterName = OUString::fromUtf8(pUpdaterName); + createStr(aUpdaterName, pArgs, 0); + } + { + // directory with the patch log + OUString aPatchDir = Updater::getPatchDirURL(); + rtl::Bootstrap::expandMacros(aPatchDir); + OUString aTempDirPath = getPathFromURL(aPatchDir); + Updater::log("Patch Dir: " + aTempDirPath); + createStr(aTempDirPath, pArgs, 1); + } + { + // the actual update directory + Updater::log("Install Dir: " + aInstallDir); + createStr(aInstallDir, pArgs, 2); + } + { + // the temporary updated build + Updater::log("Working Dir: " + aInstallDir); + createStr(aInstallDir, pArgs, 3); + } + { +#ifdef UNX + OUString aPID("0"); +#elif defined(_WIN32) + oslProcessInfo aInfo; + aInfo.Size = sizeof(oslProcessInfo); + osl_getProcessInfo(nullptr, osl_Process_IDENTIFIER, &aInfo); + OUString aPID = OUString::number(aInfo.Ident); +#else +#error "Need an implementation" +#endif + createStr(aPID, pArgs, 4); + } + { + OUString aExeDir = Updater::getExecutableDirURL(); + OUString aSofficePath = getPathFromURL(aExeDir); + Updater::log("soffice Path: " + aSofficePath); + createStr(aSofficePath, pArgs, 5); + } + { + // the executable to start after the successful update + OUString aExeDir = Updater::getExecutableDirURL(); + OUString aSofficePathURL = aExeDir + OUString::fromUtf8(pSofficeExeName); + OUString aSofficePath = getPathFromURL(aSofficePathURL); + createStr(aSofficePath, pArgs, 6); + } + + // add the command line arguments from the soffice list + for (size_t i = 0; i < nCommandLineArgs; ++i) + { + OUString aCommandLineArg; + rtl_getAppCommandArg(i, &aCommandLineArg.pData); + createStr(aCommandLineArg, pArgs, 7 + i); + } + + pArgs[nArgs - 1] = nullptr; + + return pArgs; +} + +struct update_file +{ + OUString aURL; + OUString aHash; + size_t nSize; +}; + +struct language_file +{ + update_file aUpdateFile; + OUString aLangCode; +}; + +struct update_info +{ + OUString aFromBuildID; + OUString aSeeAlsoURL; + OUString aMessage; + + update_file aUpdateFile; + std::vector<language_file> aLanguageFiles; +}; + +bool isUserWritable(const OUString& rFileURL) +{ + osl::FileStatus aStatus(osl_FileStatus_Mask_Attributes); + osl::DirectoryItem aDirectoryItem; + + osl::FileBase::RC eRes = osl::DirectoryItem::get(rFileURL, aDirectoryItem); + if (eRes != osl::FileBase::E_None) + { + Updater::log("Could not get the directory item for: " + rFileURL); + return false; + } + + osl::FileBase::RC eResult = aDirectoryItem.getFileStatus(aStatus); + if (eResult != osl::FileBase::E_None) + { + Updater::log("Could not get the file status for: " + rFileURL); + return false; + } + + bool bReadOnly = (aStatus.getAttributes() & static_cast<sal_uInt64>(osl_File_Attribute_ReadOnly)) != 0; + if (bReadOnly) + { + Updater::log("Update location as determined by: " + rFileURL + " is read-only."); + return false; + } + + return true; +} + +} + +bool update() +{ + utl::TempFile aTempDir(nullptr, true); + OUString aTempDirURL = aTempDir.GetURL(); + CopyUpdaterToTempDir(Updater::getExecutableDirURL(), aTempDirURL); + + OUString aUpdaterPath = getPathFromURL(aTempDirURL + "/" + OUString::fromUtf8(pUpdaterName)); + + Updater::log("Calling the updater with parameters: "); + CharT** pArgs = createCommandLine(); + + bool bSuccess = true; + const char* pUpdaterTestReplace = std::getenv("LIBO_UPDATER_TEST_REPLACE"); + if (!pUpdaterTestReplace) + { +#if UNX + OString aPath = OUStringToOString(aUpdaterPath, RTL_TEXTENCODING_UTF8); + if (execv(aPath.getStr(), pArgs)) + { + printf("execv failed with error %d %s\n",errno,strerror(errno)); + bSuccess = false; + } +#elif defined(_WIN32) + bSuccess = WinLaunchChild((wchar_t*)aUpdaterPath.getStr(), 8, pArgs); +#endif + } + else + { + SAL_WARN("desktop.updater", "Updater executable path: " << aUpdaterPath); + for (size_t i = 0; i < 8 + rtl_getAppCommandArgCount(); ++i) + { + SAL_WARN("desktop.updater", pArgs[i]); + } + bSuccess = false; + } + + for (size_t i = 0; i < 8 + rtl_getAppCommandArgCount(); ++i) + { + delete[] pArgs[i]; + } + delete[] pArgs; + + return bSuccess; +} + +namespace { + +// Callback to get the response data from server. +size_t WriteCallback(void *ptr, size_t size, + size_t nmemb, void *userp) +{ + if (!userp) + return 0; + + std::string* response = static_cast<std::string *>(userp); + size_t real_size = size * nmemb; + response->append(static_cast<char *>(ptr), real_size); + return real_size; +} + + + +class invalid_update_info : public std::exception +{ +}; + +class invalid_hash : public std::exception +{ + OString maMessage; +public: + + invalid_hash(const OUString& rExpectedHash, const OUString& rReceivedHash) + : maMessage( + OUStringToOString( + OUString("Invalid hash found.\nExpected: " + rExpectedHash + ";\nReceived: " + rReceivedHash), + RTL_TEXTENCODING_UTF8) + ) + { + } + + const char* what() const noexcept override + { + return maMessage.getStr(); + } +}; + +class invalid_size : public std::exception +{ + OString maMessage; +public: + + invalid_size(const size_t nExpectedSize, const size_t nReceivedSize) + : maMessage( + OUStringToOString( + OUString("Invalid file size found.\nExpected: " + OUString::number(nExpectedSize) + ";\nReceived: " + OUString::number(nReceivedSize)), + RTL_TEXTENCODING_UTF8) + ) + { + } + + const char* what() const noexcept override + { + return maMessage.getStr(); + } +}; + +OUString toOUString(const std::string& rStr) +{ + return OUString::fromUtf8(rStr.c_str()); +} + +update_file parse_update_file(orcus::json::node& rNode) +{ + if (rNode.type() != orcus::json::node_t::object) + { + SAL_WARN("desktop.updater", "invalid update or language file entry"); + throw invalid_update_info(); + } + + if (rNode.child_count() < 4) + { + SAL_WARN("desktop.updater", "invalid update or language file entry"); + throw invalid_update_info(); + } + + orcus::json::node aURLNode = rNode.child("url"); + orcus::json::node aHashNode = rNode.child("hash"); + orcus::json::node aHashTypeNode = rNode.child("hash_function"); + orcus::json::node aSizeNode = rNode.child("size"); + + if (aHashTypeNode.string_value() != "sha512") + { + SAL_WARN("desktop.updater", "invalid hash type"); + throw invalid_update_info(); + } + + update_file aUpdateFile; + aUpdateFile.aURL = toOUString(aURLNode.string_value().str()); + + if (aUpdateFile.aURL.isEmpty()) + throw invalid_update_info(); + + aUpdateFile.aHash = toOUString(aHashNode.string_value().str()); + aUpdateFile.nSize = static_cast<sal_uInt32>(aSizeNode.numeric_value()); + return aUpdateFile; +} + +update_info parse_response(const std::string& rResponse) +{ + orcus::json::document_tree aJsonDoc; + orcus::json_config aConfig; + aJsonDoc.load(rResponse, aConfig); + + auto aDocumentRoot = aJsonDoc.get_document_root(); + if (aDocumentRoot.type() != orcus::json::node_t::object) + { + SAL_WARN("desktop.updater", "invalid root entries: " << rResponse); + throw invalid_update_info(); + } + + auto aRootKeys = aDocumentRoot.keys(); + if (std::find(aRootKeys.begin(), aRootKeys.end(), "error") != aRootKeys.end()) + { + throw invalid_update_info(); + } + else if (std::find(aRootKeys.begin(), aRootKeys.end(), "response") != aRootKeys.end()) + { + update_info aUpdateInfo; + auto aMsgNode = aDocumentRoot.child("response"); + aUpdateInfo.aMessage = toOUString(aMsgNode.string_value().str()); + return aUpdateInfo; + } + + orcus::json::node aFromNode = aDocumentRoot.child("from"); + if (aFromNode.type() != orcus::json::node_t::string) + { + throw invalid_update_info(); + } + + orcus::json::node aSeeAlsoNode = aDocumentRoot.child("see also"); + if (aSeeAlsoNode.type() != orcus::json::node_t::string) + { + throw invalid_update_info(); + } + + orcus::json::node aUpdateNode = aDocumentRoot.child("update"); + if (aUpdateNode.type() != orcus::json::node_t::object) + { + throw invalid_update_info(); + } + + orcus::json::node aLanguageNode = aDocumentRoot.child("languages"); + if (aUpdateNode.type() != orcus::json::node_t::object) + { + throw invalid_update_info(); + } + + update_info aUpdateInfo; + aUpdateInfo.aFromBuildID = toOUString(aFromNode.string_value().str()); + aUpdateInfo.aSeeAlsoURL = toOUString(aSeeAlsoNode.string_value().str()); + + aUpdateInfo.aUpdateFile = parse_update_file(aUpdateNode); + + std::vector<orcus::pstring> aLanguages = aLanguageNode.keys(); + for (auto const& language : aLanguages) + { + language_file aLanguageFile; + auto aLangEntry = aLanguageNode.child(language); + aLanguageFile.aLangCode = toOUString(language.str()); + aLanguageFile.aUpdateFile = parse_update_file(aLangEntry); + aUpdateInfo.aLanguageFiles.push_back(aLanguageFile); + } + + return aUpdateInfo; +} + +struct WriteDataFile +{ + comphelper::Hash maHash; + SvStream* mpStream; + + WriteDataFile(SvStream* pStream): + maHash(comphelper::HashType::SHA512), + mpStream(pStream) + { + } + + OUString getHash() + { + auto final_hash = maHash.finalize(); + std::stringstream aStrm; + for (auto& i: final_hash) + { + aStrm << std::setw(2) << std::setfill('0') << std::hex << (int)i; + } + + return toOUString(aStrm.str()); + } +}; + +// Callback to get the response data from server to a file. +size_t WriteCallbackFile(void *ptr, size_t size, + size_t nmemb, void *userp) +{ + if (!userp) + return 0; + + WriteDataFile* response = static_cast<WriteDataFile *>(userp); + size_t real_size = size * nmemb; + response->mpStream->WriteBytes(ptr, real_size); + response->maHash.update(static_cast<const unsigned char*>(ptr), real_size); + return real_size; +} + +std::string download_content(const OString& rURL, bool bFile, OUString& rHash) +{ + Updater::log("Download: " + rURL); + std::unique_ptr<CURL, std::function<void(CURL *)>> curl( + curl_easy_init(), [](CURL * p) { curl_easy_cleanup(p); }); + + if (!curl) + return std::string(); + + curl_easy_setopt(curl.get(), CURLOPT_URL, rURL.getStr()); + curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, kUserAgent); + bool bUseProxy = false; + if (bUseProxy) + { + /* + curl_easy_setopt(curl, CURLOPT_PROXY, proxy.c_str()); + curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, proxy_user_pwd.c_str()); + */ + } + + char buf[] = "Expect:"; + curl_slist* headerlist = nullptr; + headerlist = curl_slist_append(headerlist, buf); + curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headerlist); + curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1); // follow redirects + // only allow redirect to https:// +#if (LIBCURL_VERSION_MAJOR > 7) || (LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR >= 85) + curl_easy_setopt(curl.get(), CURLOPT_REDIR_PROTOCOLS_STR, "https"); +#else + curl_easy_setopt(curl.get(), CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS); +#endif + + std::string response_body; + utl::TempFile aTempFile; + WriteDataFile aFile(aTempFile.GetStream(StreamMode::WRITE)); + if (!bFile) + { + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, + static_cast<void *>(&response_body)); + + aTempFile.EnableKillingFile(true); + } + else + { + OUString aTempFileURL = aTempFile.GetURL(); + OString aTempFileURLOString = OUStringToOString(aTempFileURL, RTL_TEXTENCODING_UTF8); + response_body.append(aTempFileURLOString.getStr(), aTempFileURLOString.getLength()); + + aTempFile.EnableKillingFile(false); + + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallbackFile); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, + static_cast<void *>(&aFile)); + } + + // Fail if 400+ is returned from the web server. + curl_easy_setopt(curl.get(), CURLOPT_FAILONERROR, 1); + + CURLcode cc = curl_easy_perform(curl.get()); + long http_code = 0; + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code); + if (http_code != 200) + { + SAL_WARN("desktop.updater", "download did not succeed. Error code: " << http_code); + throw error_updater("download did not succeed"); + } + + if (cc != CURLE_OK) + { + SAL_WARN("desktop.updater", "curl error: " << cc); + throw error_updater("curl error"); + } + + if (bFile) + rHash = aFile.getHash(); + + return response_body; +} + +void handle_file_error(osl::FileBase::RC eError, const OUString& rMsg) +{ + switch (eError) + { + case osl::FileBase::E_None: + break; + default: + SAL_WARN("desktop.updater", "file error code: " << eError << ", " << rMsg); + throw error_updater(OUStringToOString(rMsg, RTL_TEXTENCODING_UTF8)); + } +} + +void download_file(const OUString& rURL, size_t nFileSize, const OUString& rHash, const OUString& aFileName) +{ + Updater::log("Download File: " + rURL + "; FileName: " + aFileName); + OString aURL = OUStringToOString(rURL, RTL_TEXTENCODING_UTF8); + OUString aHash; + std::string temp_file = download_content(aURL, true, aHash); + if (temp_file.empty()) + throw error_updater("empty temp file string"); + + OUString aTempFile = OUString::fromUtf8(temp_file.c_str()); + Updater::log("TempFile: " + aTempFile); + osl::File aDownloadedFile(aTempFile); + osl::FileBase::RC eError = aDownloadedFile.open(1); + handle_file_error(eError, "Could not open the download file: " + aTempFile); + + sal_uInt64 nSize = 0; + eError = aDownloadedFile.getSize(nSize); + handle_file_error(eError, "Could not get the file size of the downloaded file: " + aTempFile); + if (nSize != nFileSize) + { + SAL_WARN("desktop.updater", "File sizes don't match. File might be corrupted."); + throw invalid_size(nFileSize, nSize); + } + + if (aHash != rHash) + { + SAL_WARN("desktop.updater", "File hash don't match. File might be corrupted."); + throw invalid_hash(rHash, aHash); + } + + OUString aPatchDirURL("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/patch/"); + rtl::Bootstrap::expandMacros(aPatchDirURL); + osl::Directory::create(aPatchDirURL); + + OUString aDestFile = aPatchDirURL + aFileName; + Updater::log("Destination File: " + aDestFile); + aDownloadedFile.close(); + eError = osl::File::move(aTempFile, aDestFile); + handle_file_error(eError, "Could not move the file from the Temp directory to the user config: TempFile: " + aTempFile + "; DestFile: " + aDestFile); +} + +} + +void update_checker() +{ + OUString aBrandBaseDir("${BRAND_BASE_DIR}"); + rtl::Bootstrap::expandMacros(aBrandBaseDir); + bool bUserWritable = isUserWritable(aBrandBaseDir); + if (!bUserWritable) + { + Updater::log("Can't update as the update location is not user writable"); + return; + } + + OUString aDownloadCheckBaseURL = officecfg::Office::Update::Update::URL::get(); + static const char* pDownloadCheckBaseURLEnv = std::getenv("LIBO_UPDATER_URL"); + if (pDownloadCheckBaseURLEnv) + { + aDownloadCheckBaseURL = OUString::createFromAscii(pDownloadCheckBaseURLEnv); + } + + OUString aProductName = utl::ConfigManager::getProductName(); + OUString aBuildID = Updater::getBuildID(); + + static const char* pBuildIdEnv = std::getenv("LIBO_UPDATER_BUILD"); + if (pBuildIdEnv) + { + aBuildID = OUString::createFromAscii(pBuildIdEnv); + } + + OUString aBuildTarget = "${_OS}_${_ARCH}"; + rtl::Bootstrap::expandMacros(aBuildTarget); + OUString aChannel = Updater::getUpdateChannel(); + static const char* pUpdateChannelEnv = std::getenv("LIBO_UPDATER_CHANNEL"); + if (pUpdateChannelEnv) + { + aChannel = OUString::createFromAscii(pUpdateChannelEnv); + } + + OUString aDownloadCheckURL = aDownloadCheckBaseURL + "update/check/1/" + aProductName + + "/" + aBuildID + "/" + aBuildTarget + "/" + aChannel; + OString aURL = OUStringToOString(aDownloadCheckURL, RTL_TEXTENCODING_UTF8); + Updater::log("Update check: " + aURL); + + try + { + OUString aHash; + std::string response_body = download_content(aURL, false, aHash); + if (!response_body.empty()) + { + + update_info aUpdateInfo = parse_response(response_body); + if (aUpdateInfo.aUpdateFile.aURL.isEmpty()) + { + // No update currently available + // add entry to updating.log with the message + SAL_WARN("desktop.updater", "Message received from the updater: " << aUpdateInfo.aMessage); + Updater::log("Server response: " + aUpdateInfo.aMessage); + } + else + { + css::uno::Sequence<OUString> aInstalledLanguages(officecfg::Setup::Office::InstalledLocales::get()->getElementNames()); + std::set<OUString> aInstalledLanguageSet(std::begin(aInstalledLanguages), std::end(aInstalledLanguages)); + download_file(aUpdateInfo.aUpdateFile.aURL, aUpdateInfo.aUpdateFile.nSize, aUpdateInfo.aUpdateFile.aHash, "update.mar"); + for (auto& lang_update : aUpdateInfo.aLanguageFiles) + { + // only download the language packs for installed languages + if (aInstalledLanguageSet.find(lang_update.aLangCode) != aInstalledLanguageSet.end()) + { + OUString aFileName = "update_" + lang_update.aLangCode + ".mar"; + download_file(lang_update.aUpdateFile.aURL, lang_update.aUpdateFile.nSize, lang_update.aUpdateFile.aHash, aFileName); + } + } + OUString aSeeAlsoURL = aUpdateInfo.aSeeAlsoURL; + std::shared_ptr< comphelper::ConfigurationChanges > batch( + comphelper::ConfigurationChanges::create()); + officecfg::Office::Update::Update::SeeAlso::set(aSeeAlsoURL, batch); + batch->commit(); + } + } + } + catch (const invalid_update_info&) + { + SAL_WARN("desktop.updater", "invalid update information"); + Updater::log(OString("warning: invalid update info")); + } + catch (const error_updater& e) + { + SAL_WARN("desktop.updater", "error during the update check: " << e.what()); + Updater::log(OString("warning: error by the updater") + e.what()); + } + catch (const invalid_size& e) + { + SAL_WARN("desktop.updater", e.what()); + Updater::log(OString("warning: invalid size")); + } + catch (const invalid_hash& e) + { + SAL_WARN("desktop.updater", e.what()); + Updater::log(OString("warning: invalid hash")); + } + catch (...) + { + SAL_WARN("desktop.updater", "unknown error during the update check"); + Updater::log(OString("warning: unknown exception")); + } +} + +OUString Updater::getUpdateInfoLog() +{ + OUString aUpdateInfoURL("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/patch/updating.log"); + rtl::Bootstrap::expandMacros(aUpdateInfoURL); + + return aUpdateInfoURL; +} + +OUString Updater::getPatchDirURL() +{ + OUString aPatchDirURL("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/patch/"); + rtl::Bootstrap::expandMacros(aPatchDirURL); + + return aPatchDirURL; +} + +OUString Updater::getUpdateFileURL() +{ + return getPatchDirURL() + "update.mar"; +} + +OUString Updater::getInstallationPath() +{ + OUString aInstallDir( "$BRAND_BASE_DIR/"); + rtl::Bootstrap::expandMacros(aInstallDir); + + return getPathFromURL(aInstallDir); +} + +OUString Updater::getExecutableDirURL() +{ + OUString aExeDir( "$BRAND_BASE_DIR/" LIBO_BIN_FOLDER "/" ); + rtl::Bootstrap::expandMacros(aExeDir); + + return aExeDir; +} + +void Updater::log(const OUString& rMessage) +{ + SAL_INFO("desktop.updater", rMessage); + OUString aUpdateLog = getUpdateInfoLog(); + SvFileStream aLog(aUpdateLog, StreamMode::STD_READWRITE); + aLog.Seek(aLog.Tell() + aLog.remainingSize()); // make sure we are at the end + aLog.WriteLine(OUStringToOString(rMessage, RTL_TEXTENCODING_UTF8)); +} + +void Updater::log(const OString& rMessage) +{ + SAL_INFO("desktop.updater", rMessage); + OUString aUpdateLog = getUpdateInfoLog(); + SvFileStream aLog(aUpdateLog, StreamMode::STD_READWRITE); + aLog.Seek(aLog.Tell() + aLog.remainingSize()); // make sure we are at the end + aLog.WriteLine(rMessage); +} + +void Updater::log(const char* pMessage) +{ + SAL_INFO("desktop.updater", pMessage); + OUString aUpdateLog = getUpdateInfoLog(); + SvFileStream aLog(aUpdateLog, StreamMode::STD_READWRITE); + aLog.Seek(aLog.Tell() + aLog.remainingSize()); // make sure we are at the end + aLog.WriteCharPtr(pMessage); +} + +OUString Updater::getBuildID() +{ + OUString aBuildID("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("version") ":buildid}"); + rtl::Bootstrap::expandMacros(aBuildID); + + return aBuildID; +} + +OUString Updater::getUpdateChannel() +{ + OUString aUpdateChannel("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("version") ":UpdateChannel}"); + rtl::Bootstrap::expandMacros(aUpdateChannel); + + return aUpdateChannel; +} + +void Updater::removeUpdateFiles() +{ + Updater::log("Removing: " + getUpdateFileURL()); + osl::File::remove(getUpdateFileURL()); + + OUString aPatchDirURL = getPatchDirURL(); + osl::Directory aDir(aPatchDirURL); + aDir.open(); + + osl::FileBase::RC eRC; + do + { + osl::DirectoryItem aItem; + eRC = aDir.getNextItem(aItem); + if (eRC == osl::FileBase::E_None) + { + osl::FileStatus aStatus(osl_FileStatus_Mask_All); + if (aItem.getFileStatus(aStatus) != osl::FileBase::E_None) + continue; + + if (!aStatus.isRegular()) + continue; + + OUString aURL = aStatus.getFileURL(); + if (!aURL.endsWith(".mar")) + continue; + + Updater::log("Removing. " + aURL); + osl::File::remove(aURL); + } + } + while (eRC == osl::FileBase::E_None); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/updater.hxx b/desktop/source/app/updater.hxx new file mode 100644 index 000000000..7f1ea920f --- /dev/null +++ b/desktop/source/app/updater.hxx @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + */ + +#pragma once + +#include <rtl/ustring.hxx> + +bool update(); + +void update_checker(); + +class Updater +{ +public: + static OUString getUpdateInfoLog(); + static OUString getPatchDirURL(); + static OUString getUpdateFileURL(); + static OUString getExecutableDirURL(); + static OUString getInstallationPath(); + + static OUString getBuildID(); + static OUString getUpdateChannel(); + + static void log(const OUString& rMessage); + static void log(const OString& rMessage); + static void log(const char* pMessage); + + static void removeUpdateFiles(); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/userinstall.cxx b/desktop/source/app/userinstall.cxx new file mode 100644 index 000000000..669a7316c --- /dev/null +++ b/desktop/source/app/userinstall.cxx @@ -0,0 +1,176 @@ +/* -*- 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <sal/config.h> + +#include <cassert> + +#include <com/sun/star/uno/Exception.hpp> +#include <comphelper/configuration.hxx> +#include <config_folders.h> +#include <officecfg/Setup.hxx> +#include <osl/file.h> +#include <osl/file.hxx> +#if defined ANDROID || defined IOS || defined EMSCRIPTEN +#include <rtl/bootstrap.hxx> +#endif +#include <rtl/ustring.hxx> +#include <tools/diagnose_ex.h> +#include <unotools/bootstrap.hxx> + +#include "userinstall.hxx" + +namespace desktop::userinstall { + +namespace { + +#if !(defined ANDROID || defined IOS || defined EMSCRIPTEN) +osl::FileBase::RC copyRecursive( + OUString const & srcUri, OUString const & dstUri) +{ + osl::DirectoryItem item; + osl::FileBase::RC e = osl::DirectoryItem::get(srcUri, item); + if (e != osl::FileBase::E_None) { + return e; + } + osl::FileStatus stat1(osl_FileStatus_Mask_Type); + e = item.getFileStatus(stat1); + if (e != osl::FileBase::E_None) { + return e; + } + if (stat1.getFileType() == osl::FileStatus::Directory) { + e = osl::Directory::create(dstUri); + if (e != osl::FileBase::E_None && e != osl::FileBase::E_EXIST) { + return e; + } + osl::Directory dir(srcUri); + e = dir.open(); + if (e != osl::FileBase::E_None) { + return e; + } + for (;;) { + e = dir.getNextItem(item); + if (e == osl::FileBase::E_NOENT) { + break; + } + if (e != osl::FileBase::E_None) { + return e; + } + osl::FileStatus stat2( + osl_FileStatus_Mask_FileName | osl_FileStatus_Mask_FileURL); + e = item.getFileStatus(stat2); + if (e != osl::FileBase::E_None) { + return e; + } + assert(!dstUri.endsWith("/")); + e = copyRecursive( + stat2.getFileURL(), dstUri + "/" + stat2.getFileName()); + // assumes that all files under presets/ have names that can be + // copied unencoded into file URLs + if (e != osl::FileBase::E_None) { + return e; + } + } + e = dir.close(); + } else { + e = osl::File::copy(srcUri, dstUri); + if (e == osl::FileBase::E_EXIST) { + // Assume an earlier attempt failed half-way through: + e = osl::FileBase::E_None; + } + } + return e; +} +#endif + +Status create(OUString const & uri) { + osl::FileBase::RC e = osl::Directory::createPath(uri); + if (e != osl::FileBase::E_None && e != osl::FileBase::E_EXIST) { + return ERROR_OTHER; + } +#if !(defined ANDROID || defined IOS || defined EMSCRIPTEN) +#if defined UNIX + // Set safer permissions for the user directory by default: + osl::File::setAttributes( + uri, + (osl_File_Attribute_OwnWrite | osl_File_Attribute_OwnRead + | osl_File_Attribute_OwnExe)); +#endif + // As of now osl_copyFile does not work on Android => don't do this: + OUString baseUri; + if (utl::Bootstrap::locateBaseInstallation(baseUri) + != utl::Bootstrap::PATH_EXISTS) + { + return ERROR_OTHER; + } + switch (copyRecursive( + baseUri + "/" LIBO_SHARE_PRESETS_FOLDER, uri + "/user")) + { + case osl::FileBase::E_None: + break; + case osl::FileBase::E_ACCES: + return ERROR_CANT_WRITE; + case osl::FileBase::E_NOSPC: + return ERROR_NO_SPACE; + default: + return ERROR_OTHER; + } +#else + // On (Android and) iOS, just create the user directory. Later code fails mysteriously if it + // doesn't exist. + OUString userDir("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/user"); + rtl::Bootstrap::expandMacros(userDir); + osl::Directory::createPath(userDir); +#endif + std::shared_ptr<comphelper::ConfigurationChanges> batch( + comphelper::ConfigurationChanges::create()); + officecfg::Setup::Office::ooSetupInstCompleted::set(true, batch); + batch->commit(); + return CREATED; +} + +bool isCreated() { + try { + return officecfg::Setup::Office::ooSetupInstCompleted::get(); + } catch (const css::uno::Exception &) { + TOOLS_WARN_EXCEPTION("desktop.app", "ignoring"); + return false; + } +} + +} + +Status finalize() { + OUString uri; + switch (utl::Bootstrap::locateUserInstallation(uri)) { + case utl::Bootstrap::PATH_EXISTS: + if (isCreated()) { + return EXISTED; + } + [[fallthrough]]; + case utl::Bootstrap::PATH_VALID: + return create(uri); + default: + return ERROR_OTHER; + } +} + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/desktop/source/app/userinstall.hxx b/desktop/source/app/userinstall.hxx new file mode 100644 index 000000000..527df2dcc --- /dev/null +++ b/desktop/source/app/userinstall.hxx @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * 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/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include <sal/config.h> + +namespace desktop::userinstall +{ +enum Status +{ + EXISTED, + CREATED, + ERROR_NO_SPACE, + ERROR_CANT_WRITE, + ERROR_OTHER +}; + +Status finalize(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |