/* -*- 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 #include "unopkg_main.h" #include "unopkg_shared.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(UNX) #include #endif #include #include #include using namespace ::com::sun::star; using namespace ::com::sun::star::logging; using namespace ::com::sun::star::uno; using namespace ::unopkg; namespace { struct ExtensionName { OUString m_str; explicit ExtensionName( OUString str ) : m_str(std::move( str )) {} bool operator () ( Reference const & e ) const { return m_str == dp_misc::getIdentifier(e) || m_str == e->getName(); } }; const char16_t s_usingText [] = u"\n" "using: " APP_NAME " add extension-path...\n" " " APP_NAME " validate extension-identifier...\n" " " APP_NAME " remove extension-identifier...\n" " " APP_NAME " list extension-identifier...\n" " " APP_NAME " reinstall \n" " " APP_NAME " gui\n" " " APP_NAME " -V\n" " " APP_NAME " -h\n" "\n" "sub-commands:\n" " add add extension\n" " validate checks the prerequisites of an installed extension and\n" " registers it if possible\n" " remove remove extensions by identifier\n" " reinstall expert feature: reinstall all deployed extensions\n" " list list information about deployed extensions\n" " gui raise Extensions dialog\n" "\n" "options:\n" " -h, --help this help\n" " -V, --version version information\n" " -v, --verbose verbose output\n" " -f, --force force overwriting existing extensions\n" " -s, --suppress-license prevents showing the license\n" " --log-file custom log file; default: /log.txt\n" " --shared expert feature: operate on shared installation\n" " deployment context;\n" " run only when no concurrent Office\n" " process(es) are running!\n" " --bundled expert feature: operate on bundled extensions. Only\n" " works with list, validate, reinstall;\n" " --deployment-context expert feature: explicit deployment context\n" " \n" "\n" "To learn more about extensions, see:\n" "https://wiki.documentfoundation.org/Documentation/DevGuide/Extensions\n\n"; const OptionInfo s_option_infos [] = { { RTL_CONSTASCII_STRINGPARAM("help"), 'h', false }, { RTL_CONSTASCII_STRINGPARAM("version"), 'V', false }, { RTL_CONSTASCII_STRINGPARAM("verbose"), 'v', false }, { RTL_CONSTASCII_STRINGPARAM("force"), 'f', false }, { RTL_CONSTASCII_STRINGPARAM("log-file"), '\0', true }, { RTL_CONSTASCII_STRINGPARAM("shared"), '\0', false }, { RTL_CONSTASCII_STRINGPARAM("deployment-context"), '\0', true }, { RTL_CONSTASCII_STRINGPARAM("bundled"), '\0', false}, { RTL_CONSTASCII_STRINGPARAM("suppress-license"), 's', false}, { nullptr, 0, '\0', false } }; void logFatal( comphelper::EventLogger const * logger, sal_Int32 level, OUString const & message, OUString const & argument) { if (logger == nullptr) { // Best effort; potentially loses data due to conversion failures (stray surrogate halves) // and embedded null characters: std::cerr << OUStringToOString(message.replaceFirst("$1$", argument), RTL_TEXTENCODING_UTF8) << '\n'; } else { logger->log(level, message, argument); } } class DialogClosedListenerImpl : public ::cppu::WeakImplHelper< ui::dialogs::XDialogClosedListener > { osl::Condition & m_rDialogClosedCondition; public: explicit DialogClosedListenerImpl( osl::Condition & rDialogClosedCondition ) : m_rDialogClosedCondition( rDialogClosedCondition ) {} // XEventListener (base of XDialogClosedListener) virtual void SAL_CALL disposing( lang::EventObject const & Source ) override; // XDialogClosedListener virtual void SAL_CALL dialogClosed( ui::dialogs::DialogClosedEvent const & aEvent ) override; }; // XEventListener (base of XDialogClosedListener) void DialogClosedListenerImpl::disposing( lang::EventObject const & ) { // nothing to do } // XDialogClosedListener void DialogClosedListenerImpl::dialogClosed( ui::dialogs::DialogClosedEvent const & ) { m_rDialogClosedCondition.set(); } // If a package had been installed with a pre OOo 2.2, it could not normally be // found via its identifier; similarly (and for ease of use), a package // installed with OOo 2.2 or later could not normally be found via its file // name. Reference findPackage( OUString const & repository, Reference const & manager, Reference const & environment, std::u16string_view idOrFileName ) { const Sequence< Reference > ps( manager->getDeployedExtensions(repository, Reference(), environment ) ); for ( auto const & package : ps ) if ( dp_misc::getIdentifier( package ) == idOrFileName ) return package; for ( auto const & package : ps ) if ( package->getName() == idOrFileName ) return package; return Reference(); } } // anon namespace extern "C" int unopkg_main() { tools::extendApplicationEnvironment(); bool bShowFailedMsg = true; OUString subCommand; bool option_shared = false; bool option_force = false; bool option_verbose = false; bool option_bundled = false; bool option_suppressLicense = false; bool option_help = false; bool subcmd_gui = false; OUString logFile; OUString repository; OUString cmdArg; std::vector cmdPackages; Reference xFileHandler; Reference xConsoleHandler; std::unique_ptr logger; std::unique_ptr pUserProfileTempDir; OptionInfo const * info_shared = getOptionInfo( s_option_infos, "shared" ); OptionInfo const * info_force = getOptionInfo( s_option_infos, "force" ); OptionInfo const * info_verbose = getOptionInfo( s_option_infos, "verbose" ); OptionInfo const * info_log = getOptionInfo( s_option_infos, "log-file" ); OptionInfo const * info_context = getOptionInfo( s_option_infos, "deployment-context" ); OptionInfo const * info_help = getOptionInfo( s_option_infos, "help" ); OptionInfo const * info_version = getOptionInfo( s_option_infos, "version" ); OptionInfo const * info_bundled = getOptionInfo( s_option_infos, "bundled" ); OptionInfo const * info_suppressLicense = getOptionInfo( s_option_infos, "suppress-license" ); Reference xComponentContext; Reference xLocalComponentContext; try { sal_uInt32 nPos = 0; sal_uInt32 nCount = osl_getCommandArgCount(); if (nCount == 0 || isOption( info_help, &nPos )) { dp_misc::writeConsole(s_usingText); return 0; } else if (isOption( info_version, &nPos )) { dp_misc::writeConsole(u"\n" APP_NAME " Version 3.3\n"); return 0; } //consume all bootstrap variables which may occur before the sub-command while(isBootstrapVariable(&nPos)) ; if(nPos >= nCount) return 0; //get the sub-command osl_getCommandArg( nPos, &subCommand.pData ); ++nPos; subCommand = subCommand.trim(); bool subcmd_add = subCommand == "add"; subcmd_gui = subCommand == "gui"; // sub-command options and packages: while (nPos < nCount) { if (readArgument( &cmdArg, info_log, &nPos )) { logFile = makeAbsoluteFileUrl( cmdArg.trim(), getProcessWorkingDir() ); } else if (!readOption( &option_verbose, info_verbose, &nPos ) && !readOption( &option_shared, info_shared, &nPos ) && !readOption( &option_force, info_force, &nPos ) && !readOption( &option_bundled, info_bundled, &nPos ) && !readOption( &option_suppressLicense, info_suppressLicense, &nPos ) && !readOption( &option_help, info_help, &nPos ) && !readArgument( &repository, info_context, &nPos ) && !isBootstrapVariable(&nPos)) { osl_getCommandArg( nPos, &cmdArg.pData ); ++nPos; cmdArg = cmdArg.trim(); if (!cmdArg.isEmpty()) { if (cmdArg[ 0 ] == '-') { // is option: dp_misc::writeConsoleError(Concat2View( "\nERROR: unexpected option " + cmdArg + "!\n Use " APP_NAME " " + toString(info_help) + " to print all options.\n")); return 1; } else { // is package: cmdPackages.push_back( subcmd_add || subcmd_gui ? makeAbsoluteFileUrl( cmdArg, getProcessWorkingDir() ) : cmdArg ); } } } } // tdf#129917 Use temp user profile when installing shared extensions if (option_shared) { pUserProfileTempDir.reset(new utl::TempFileNamed(nullptr, true)); pUserProfileTempDir->EnableKillingFile(); } xComponentContext = getUNO(option_verbose, subcmd_gui, pUserProfileTempDir ? pUserProfileTempDir->GetURL() : "", xLocalComponentContext); // Initialize logging. This will log errors to the console and // also to file if the --log-file parameter was provided. logger.reset(new comphelper::EventLogger(xLocalComponentContext, "unopkg")); const Reference xLogger(logger->getLogger()); xLogger->setLevel(LogLevel::WARNING); Reference xLogFormatter(SimpleTextFormatter::create(xLocalComponentContext)); Sequence < beans::NamedValue > aSeq { { "Formatter", Any(xLogFormatter) } }; xConsoleHandler.set(ConsoleHandler::createWithSettings(xLocalComponentContext, aSeq)); xLogger->addLogHandler(xConsoleHandler); xConsoleHandler->setLevel(LogLevel::WARNING); xLogger->setLevel(LogLevel::WARNING); if (!logFile.isEmpty()) { Sequence < beans::NamedValue > aSeq2 { { "Formatter", Any(xLogFormatter) }, {"FileURL", Any(logFile)} }; xFileHandler.set(css::logging::FileHandler::createWithSettings(xLocalComponentContext, aSeq2)); xFileHandler->setLevel(LogLevel::WARNING); xLogger->addLogHandler(xFileHandler); } if (option_verbose) { xLogger->setLevel(LogLevel::INFO); xConsoleHandler->setLevel(LogLevel::INFO); if (xFileHandler.is()) xFileHandler->setLevel(LogLevel::INFO); } if (repository.isEmpty()) { if (option_shared) repository = "shared"; else if (option_bundled) repository = "bundled"; else repository = "user"; } else { if ( repository == "shared" ) { option_shared = true; } else if (option_shared) { logger->log(LogLevel::WARNING, "Explicit context given! Ignoring option '$1$'", toString(info_shared)); } } #if defined(UNX) if ( geteuid() == 0 ) { if ( !(option_shared || option_bundled || option_help) ) { logger->log(LogLevel::SEVERE, "Cannot run $1$ as root without $2$ or $3$ option.", APP_NAME, toString(info_shared), toString(info_bundled)); return 1; } } #endif if (subCommand == "reinstall") { //We must prevent that services and types are loaded by UNO, //otherwise we cannot delete the registry data folder. OUString extensionUnorc; if (repository == "user") extensionUnorc = "$UNO_USER_PACKAGES_CACHE/registry/com.sun.star.comp.deployment.component.PackageRegistryBackend/unorc"; else if (repository == "shared") extensionUnorc = "$SHARED_EXTENSIONS_USER/registry/com.sun.star.comp.deployment.component.PackageRegistryBackend/unorc"; else if (repository == "bundled") extensionUnorc = "$BUNDLED_EXTENSIONS_USER/registry/com.sun.star.comp.deployment.component.PackageRegistryBackend/unorc"; else OSL_ASSERT(false); ::rtl::Bootstrap::expandMacros(extensionUnorc); oslFileError e = osl_removeFile(extensionUnorc.pData); if (e != osl_File_E_None && e != osl_File_E_NOENT) throw Exception("Could not delete " + extensionUnorc, nullptr); } Reference xExtensionManager( deployment::ExtensionManager::get( xComponentContext ) ); Reference xCmdEnv( createCmdEnv(xComponentContext, option_force, option_verbose, option_suppressLicense)); //synchronize bundled/shared extensions //Do not synchronize when command is "reinstall". This could add types and services to UNO and //prevent the deletion of the registry data folder //syncing is done in XExtensionManager.reinstall if (!subcmd_gui && subCommand != "reinstall" && ! dp_misc::office_is_running()) dp_misc::syncRepositories(false, xCmdEnv); if ( subcmd_add || subCommand == "remove" ) { for (const OUString & cmdPackage : cmdPackages) { if (subcmd_add) { beans::NamedValue nvSuppress( "SUPPRESS_LICENSE", option_suppressLicense ? Any(OUString("1")):Any(OUString("0"))); xExtensionManager->addExtension( cmdPackage, Sequence(&nvSuppress, 1), repository, Reference(), xCmdEnv); } else { try { xExtensionManager->removeExtension( cmdPackage, cmdPackage, repository, Reference(), xCmdEnv ); } catch (const lang::IllegalArgumentException &) { Reference p( findPackage(repository, xExtensionManager, xCmdEnv, cmdPackage ) ); if ( !p.is()) throw; else if (p.is()) xExtensionManager->removeExtension( ::dp_misc::getIdentifier(p), p->getName(), repository, Reference(), xCmdEnv ); } } } } else if ( subCommand == "reinstall" ) { xExtensionManager->reinstallDeployedExtensions( false, repository, Reference(), xCmdEnv); } else if ( subCommand == "list" ) { std::vector > vecExtUnaccepted; ::comphelper::sequenceToContainer(vecExtUnaccepted, xExtensionManager->getExtensionsWithUnacceptedLicenses( repository, xCmdEnv)); //This vector tells what XPackage in allExtensions has an //unaccepted license. std::vector vecUnaccepted; std::vector > allExtensions; if (cmdPackages.empty()) { Sequence< Reference > packages = xExtensionManager->getDeployedExtensions( repository, Reference(), xCmdEnv ); std::vector > vec_packages; ::comphelper::sequenceToContainer(vec_packages, packages); //First copy the extensions with the unaccepted license //to vector allExtensions. allExtensions.resize(vecExtUnaccepted.size() + vec_packages.size()); std::vector >::iterator i_all_ext = std::copy(vecExtUnaccepted.begin(), vecExtUnaccepted.end(), allExtensions.begin()); //Now copy those we got from getDeployedExtensions std::copy(vec_packages.begin(), vec_packages.end(), i_all_ext); //Now prepare the vector which tells what extension has an //unaccepted license vecUnaccepted.resize(vecExtUnaccepted.size() + vec_packages.size()); std::fill_n(vecUnaccepted.begin(), vecExtUnaccepted.size(), true); std::fill_n(vecUnaccepted.begin() + vecExtUnaccepted.size(), vec_packages.size(), false); dp_misc::writeConsole( Concat2View("All deployed " + repository + " extensions:\n\n")); } else { //The user provided the names (ids or file names) of the extensions //which shall be listed for (const OUString & cmdPackage : cmdPackages) { Reference extension; try { extension = xExtensionManager->getDeployedExtension( repository, cmdPackage, cmdPackage, xCmdEnv ); } catch (const lang::IllegalArgumentException &) { extension = findPackage(repository, xExtensionManager, xCmdEnv, cmdPackage ); } //Now look if the requested extension has an unaccepted license bool bUnacceptedLic = false; if (!extension.is()) { std::vector >::const_iterator i = std::find_if( vecExtUnaccepted.begin(), vecExtUnaccepted.end(), ExtensionName(cmdPackage)); if (i != vecExtUnaccepted.end()) { extension = *i; bUnacceptedLic = true; } } if (!extension.is()) throw lang::IllegalArgumentException( "There is no such extension deployed: " + cmdPackage,nullptr,-1); allExtensions.push_back(extension); vecUnaccepted.push_back(bUnacceptedLic); } } printf_packages(allExtensions, vecUnaccepted, xCmdEnv ); } else if ( subCommand == "validate" ) { std::vector > vecExtUnaccepted; ::comphelper::sequenceToContainer( vecExtUnaccepted, xExtensionManager->getExtensionsWithUnacceptedLicenses( repository, xCmdEnv)); for (const OUString & cmdPackage : cmdPackages) { Reference extension; try { extension = xExtensionManager->getDeployedExtension( repository, cmdPackage, cmdPackage, xCmdEnv ); } catch (const lang::IllegalArgumentException &) { extension = findPackage( repository, xExtensionManager, xCmdEnv, cmdPackage ); } if (!extension.is()) { std::vector >::const_iterator i = std::find_if( vecExtUnaccepted.begin(), vecExtUnaccepted.end(), ExtensionName(cmdPackage)); if (i != vecExtUnaccepted.end()) { extension = *i; } } if (extension.is()) xExtensionManager->checkPrerequisitesAndEnable( extension, Reference(), xCmdEnv); } } else if ( subCommand == "gui" ) { Reference xDialog( deployment::ui::PackageManagerDialog::createAndInstall( xComponentContext, !cmdPackages.empty() ? cmdPackages[0] : OUString() )); osl::Condition dialogEnded; dialogEnded.reset(); Reference< ui::dialogs::XDialogClosedListener > xListener( new DialogClosedListenerImpl( dialogEnded ) ); xDialog->startExecuteModal(xListener); dialogEnded.wait(); return 0; } else { logger->log(LogLevel::SEVERE, "Unknown sub-command: '$1$'. Use $2$ $3$ to print all options.", subCommand, APP_NAME, toString(info_help)); return 1; } logger->log(LogLevel::INFO, "$1$ done.", APP_NAME); //Force to release all bridges which connect us to the child processes dp_misc::disposeBridges(xLocalComponentContext); css::uno::Reference( xLocalComponentContext, css::uno::UNO_QUERY_THROW)->dispose(); return 0; } catch (const ucb::CommandFailedException &e) { logFatal(logger.get(), LogLevel::SEVERE, "Exception occurred: $1$", e.Message); } catch (const ucb::CommandAbortedException &) { logFatal(logger.get(), LogLevel::SEVERE, "$1$ aborted.", APP_NAME); bShowFailedMsg = false; } catch (const deployment::DeploymentException & exc) { logFatal(logger.get(), LogLevel::SEVERE, "Exception occurred: $1$", exc.Message); logFatal( logger.get(), LogLevel::INFO, " Cause: $1$", comphelper::anyToString(exc.Cause)); } catch (const LockFileException & e) { // No logger since it requires UNO which we don't have here dp_misc::writeConsoleError(Concat2View(e.Message + "\n")); bShowFailedMsg = false; } catch (const css::uno::Exception & e ) { Any exc( ::cppu::getCaughtException() ); logFatal(logger.get(), LogLevel::SEVERE, "Exception occurred: $1$", e.Message); logFatal(logger.get(), LogLevel::INFO, " Cause: $1$", comphelper::anyToString(exc)); } if (bShowFailedMsg) logFatal(logger.get(), LogLevel::SEVERE, "$1$ failed.", APP_NAME); dp_misc::disposeBridges(xLocalComponentContext); if (xLocalComponentContext.is()) { css::uno::Reference( xLocalComponentContext, css::uno::UNO_QUERY_THROW)->dispose(); } return 1; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */