/* -*- 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 #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 #include #include #include #include #include #include #include #include "additions.hxx" #include "components.hxx" #include "data.hxx" #include "lock.hxx" #include "modifications.hxx" #include "node.hxx" #include "nodemap.hxx" #include "parsemanager.hxx" #include "partial.hxx" #include "rootaccess.hxx" #include "writemodfile.hxx" #include "xcdparser.hxx" #include "xcuparser.hxx" #include "xcsparser.hxx" #if ENABLE_DCONF #include "dconf.hxx" #endif #if defined(_WIN32) #include "winreg.hxx" #endif namespace configmgr { namespace { struct UnresolvedVectorItem { OUString name; rtl::Reference< ParseManager > manager; UnresolvedVectorItem( OUString theName, rtl::Reference< ParseManager > theManager): name(std::move(theName)), manager(std::move(theManager)) {} }; typedef std::vector< UnresolvedVectorItem > UnresolvedVector; void parseXcsFile( OUString const & url, int layer, Data & data, Partial const * partial, Modifications * modifications, Additions * additions) { assert(partial == nullptr && modifications == nullptr && additions == nullptr); (void) partial; (void) modifications; (void) additions; bool ok = rtl::Reference< ParseManager >( new ParseManager(url, new XcsParser(layer, data)))->parse(nullptr); assert(ok); (void) ok; // avoid warnings } void parseXcuFile( OUString const & url, int layer, Data & data, Partial const * partial, Modifications * modifications, Additions * additions) { bool ok = rtl::Reference< ParseManager >( new ParseManager( url, new XcuParser(layer, data, partial, modifications, additions)))-> parse(nullptr); assert(ok); (void) ok; // avoid warnings } OUString expand(OUString const & str) { OUString s(str); rtl::Bootstrap::expandMacros(s); //TODO: detect failure return s; } bool canRemoveFromLayer(int layer, rtl::Reference< Node > const & node) { assert(node.is()); if (node->getLayer() > layer && node->getLayer() < Data::NO_LAYER) { return false; } switch (node->kind()) { case Node::KIND_LOCALIZED_PROPERTY: case Node::KIND_GROUP: for (auto const& member : node->getMembers()) { if (!canRemoveFromLayer(layer, member.second)) { return false; } } return true; case Node::KIND_SET: return node->getMembers().empty(); default: // Node::KIND_PROPERTY, Node::KIND_LOCALIZED_VALUE return true; } } } class Components::WriteThread: public salhelper::Thread { public: WriteThread( rtl::Reference< WriteThread > * reference, Components & components, OUString url, Data const & data); void flush() { delay_.set(); } private: virtual ~WriteThread() override {} virtual void execute() override; rtl::Reference< WriteThread > * reference_; Components & components_; OUString url_; Data const & data_; osl::Condition delay_; std::shared_ptr lock_; }; Components::WriteThread::WriteThread( rtl::Reference< WriteThread > * reference, Components & components, OUString url, Data const & data): Thread("configmgrWriter"), reference_(reference), components_(components), url_(std::move(url)), data_(data), lock_( lock() ) { assert(reference != nullptr); } void Components::WriteThread::execute() { delay_.wait(std::chrono::seconds(1)); // must not throw; result_error is harmless and ignored osl::MutexGuard g(*lock_); // must not throw try { try { writeModFile(components_, url_, data_); } catch (css::uno::RuntimeException &) { // Ignore write errors, instead of aborting: TOOLS_WARN_EXCEPTION("configmgr", "error writing modifications"); } } catch (...) { reference_->clear(); throw; } reference_->clear(); } Components & Components::getSingleton( css::uno::Reference< css::uno::XComponentContext > const & context) { assert(context.is()); static Components singleton(context); return singleton; } bool Components::allLocales(std::u16string_view locale) { return locale == u"*"; } rtl::Reference< Node > Components::resolvePathRepresentation( OUString const & pathRepresentation, OUString * canonicRepresentation, std::vector * path, int * finalizedLayer) const { return data_.resolvePathRepresentation( pathRepresentation, canonicRepresentation, path, finalizedLayer); } rtl::Reference< Node > Components::getTemplate(OUString const & fullName) const { return data_.getTemplate(Data::NO_LAYER, fullName); } void Components::addRootAccess(rtl::Reference< RootAccess > const & access) { roots_.insert(access.get()); } void Components::removeRootAccess(RootAccess * access) { roots_.erase(access); } void Components::initGlobalBroadcaster( Modifications const & modifications, rtl::Reference< RootAccess > const & exclude, Broadcaster * broadcaster) { //TODO: Iterate only over roots w/ listeners: for (auto const& elemRoot : roots_) { rtl::Reference< RootAccess > root; if (elemRoot->acquireCounting() > 1) { root.set(elemRoot); // must not throw } elemRoot->releaseNondeleting(); if (root.is()) { if (root != exclude) { std::vector path(root->getAbsolutePath()); Modifications::Node const * mods = &modifications.getRoot(); for (auto const& pathElem : path) { Modifications::Node::Children::const_iterator k( mods->children.find(pathElem)); if (k == mods->children.end()) { mods = nullptr; break; } mods = &k->second; } //TODO: If the complete tree of which root is a part is deleted, // or replaced, mods will be null, but some of the listeners // from within root should probably fire nonetheless: if (mods != nullptr) { root->initBroadcaster(*mods, broadcaster); } } } } } void Components::addModification(std::vector const & path) { data_.modifications.add(path); } void Components::writeModifications() { if (data_.modifications.empty()) return; switch (modificationTarget_) { case ModificationTarget::None: break; case ModificationTarget::File: if (!writeThread_.is()) { writeThread_ = new WriteThread( &writeThread_, *this, modificationFileUrl_, data_); writeThread_->launch(); } break; case ModificationTarget::Dconf: #if ENABLE_DCONF dconf::writeModifications(*this, data_); #endif break; } } void Components::flushModifications() { rtl::Reference< WriteThread > thread; { osl::MutexGuard g(*lock_); thread = writeThread_; } if (thread.is()) { thread->flush(); thread->join(); } } void Components::insertExtensionXcsFile( bool shared, OUString const & fileUri) { int layer = getExtensionLayer(shared); try { parseXcsFile(fileUri, layer, data_, nullptr, nullptr, nullptr); } catch (css::container::NoSuchElementException & e) { throw css::uno::RuntimeException( "insertExtensionXcsFile does not exist: " + e.Message); } } void Components::insertExtensionXcuFile( bool shared, OUString const & fileUri, Modifications * modifications) { assert(modifications != nullptr); int layer = getExtensionLayer(shared) + 1; Additions * adds = data_.addExtensionXcuAdditions(fileUri, layer); try { parseXcuFile(fileUri, layer, data_, nullptr, modifications, adds); } catch (css::container::NoSuchElementException & e) { data_.removeExtensionXcuAdditions(fileUri); throw css::uno::RuntimeException( "insertExtensionXcuFile does not exist: " + e.Message); } } void Components::removeExtensionXcuFile( OUString const & fileUri, Modifications * modifications) { //TODO: Ideally, exactly the data coming from the specified xcu file would // be removed. However, not enough information is recorded in the in-memory // data structures to do so. So, as a workaround, all those set elements // that were freshly added by the xcu and have afterwards been left // unchanged or have only had their properties changed in the user layer are // removed (and nothing else). The heuristic to determine // whether a node has been left unchanged is to check the layer ID (as // usual) and additionally to check that the node does not recursively // contain any non-empty sets (multiple extension xcu files are merged into // one layer, so checking layer ID alone is not enough). Since // item->additions records all additions of set members in textual order, // the latter check works well when iterating through item->additions in // reverse order. assert(modifications != nullptr); rtl::Reference< Data::ExtensionXcu > item( data_.removeExtensionXcuAdditions(fileUri)); if (!item.is()) return; for (Additions::reverse_iterator i(item->additions.rbegin()); i != item->additions.rend(); ++i) { rtl::Reference< Node > parent; NodeMap const * map = &data_.getComponents(); rtl::Reference< Node > node; for (auto const& j : *i) { parent = node; node = map->findNode(Data::NO_LAYER, j); if (!node.is()) { break; } map = &node->getMembers(); } if (node.is()) { assert(parent.is()); if (parent->kind() == Node::KIND_SET) { assert( node->kind() == Node::KIND_GROUP || node->kind() == Node::KIND_SET); if (canRemoveFromLayer(item->layer, node)) { parent->getMembers().erase(i->back()); data_.modifications.remove(*i); modifications->add(*i); } } } } writeModifications(); } void Components::insertModificationXcuFile( OUString const & fileUri, std::set< OUString > const & includedPaths, std::set< OUString > const & excludedPaths, Modifications * modifications) { assert(modifications != nullptr); Partial part(includedPaths, excludedPaths); try { parseFileLeniently( &parseXcuFile, fileUri, Data::NO_LAYER, &part, modifications, nullptr); } catch (const css::container::NoSuchElementException &) { TOOLS_WARN_EXCEPTION( "configmgr", "error inserting non-existing \"" << fileUri << "\""); } } css::beans::Optional< css::uno::Any > Components::getExternalValue( std::u16string_view descriptor) { size_t i = descriptor.find(' '); if (i == 0 || i == std::u16string_view::npos) { throw css::uno::RuntimeException( OUString::Concat("bad external value descriptor ") + descriptor); } //TODO: Do not make calls with mutex locked: OUString name(descriptor.substr(0, i)); ExternalServices::iterator j(externalServices_.find(name)); if (j == externalServices_.end()) { css::uno::Reference< css::uno::XInterface > service; try { service = context_->getServiceManager()->createInstanceWithContext( name, context_); } catch (const css::uno::RuntimeException &) { // Assuming these exceptions are real errors: throw; } catch (const css::uno::Exception &) { // Assuming these exceptions indicate that the service is not // installed: TOOLS_WARN_EXCEPTION( "configmgr", "createInstance(" << name << ") failed"); } css::uno::Reference< css::beans::XPropertySet > propset; if (service.is()) { propset.set( service, css::uno::UNO_QUERY_THROW); } j = externalServices_.emplace(name, propset).first; } css::beans::Optional< css::uno::Any > value; if (j->second.is()) { try { if (!(j->second->getPropertyValue(OUString(descriptor.substr(i + 1))) >>= value)) { throw css::uno::RuntimeException( OUString::Concat("cannot obtain external value through ") + descriptor); } } catch (css::beans::UnknownPropertyException & e) { throw css::uno::RuntimeException( "unknown external value descriptor ID: " + e.Message); } catch (css::lang::WrappedTargetException & e) { css::uno::Any anyEx = cppu::getCaughtException(); throw css::lang::WrappedTargetRuntimeException( "cannot obtain external value: " + e.Message, nullptr, anyEx ); } } return value; } Components::Components( css::uno::Reference< css::uno::XComponentContext > const & context): context_(context), sharedExtensionLayer_(-1), userExtensionLayer_(-1), modificationTarget_(ModificationTarget::None) { assert(context.is()); lock_ = lock(); OUString conf(expand("${CONFIGURATION_LAYERS}")); int layer = 0; for (sal_Int32 i = 0;;) { while (i != conf.getLength() && conf[i] == ' ') { ++i; } if (i == conf.getLength()) { break; } if (modificationTarget_ != ModificationTarget::None) { throw css::uno::RuntimeException( "CONFIGURATION_LAYERS: modification target layer followed by" " further layers"); } sal_Int32 c = i; for (;; ++c) { if (c == conf.getLength() || conf[c] == ' ') { throw css::uno::RuntimeException( "CONFIGURATION_LAYERS: missing ':' in \"" + conf + "\""); } if (conf[c] == ':') { break; } } sal_Int32 n = conf.indexOf(' ', c + 1); if (n == -1) { n = conf.getLength(); } OUString type(conf.copy(i, c - i)); OUString url(conf.copy(c + 1, n - c - 1)); if (type == "xcsxcu") { sal_uInt32 nStartTime = osl_getGlobalTimer(); parseXcsXcuLayer(layer, url); SAL_INFO("configmgr", "parseXcsXcuLayer() took " << (osl_getGlobalTimer() - nStartTime) << " ms"); layer += 2; //TODO: overflow } else if (type == "bundledext") { parseXcsXcuIniLayer(layer, url, false); layer += 2; //TODO: overflow } else if (type == "sharedext") { if (sharedExtensionLayer_ != -1) { throw css::uno::RuntimeException( "CONFIGURATION_LAYERS: multiple \"sharedext\" layers"); } sharedExtensionLayer_ = layer; parseXcsXcuIniLayer(layer, url, true); layer += 2; //TODO: overflow } else if (type == "userext") { if (userExtensionLayer_ != -1) { throw css::uno::RuntimeException( "CONFIGURATION_LAYERS: multiple \"userext\" layers"); } userExtensionLayer_ = layer; parseXcsXcuIniLayer(layer, url, true); layer += 2; //TODO: overflow } else if (type == "res") { sal_uInt32 nStartTime = osl_getGlobalTimer(); parseResLayer(layer, url); SAL_INFO("configmgr", "parseResLayer() took " << (osl_getGlobalTimer() - nStartTime) << " ms"); ++layer; //TODO: overflow #if ENABLE_DCONF } else if (type == "dconf") { if (url == "!") { modificationTarget_ = ModificationTarget::Dconf; dconf::readLayer(data_, Data::NO_LAYER); } else if (url == "*") { dconf::readLayer(data_, layer); } else { throw css::uno::RuntimeException( "CONFIGURATION_LAYERS: unknown \"dconf\" kind \"" + url + "\""); } ++layer; //TODO: overflow #endif #if defined(_WIN32) } else if (type == "winreg") { WinRegType eType; if (url == "LOCAL_MACHINE" || url.isEmpty()/*backwards comp.*/) { eType = WinRegType::LOCAL_MACHINE; } else if (url == "CURRENT_USER") { eType = WinRegType::CURRENT_USER; } else { throw css::uno::RuntimeException( "CONFIGURATION_LAYERS: unknown \"winreg\" kind \"" + url + "\""); } OUString aTempFileURL; if (dumpWindowsRegistry(&aTempFileURL, eType)) { parseFileLeniently(&parseXcuFile, aTempFileURL, layer, nullptr, nullptr, nullptr); if (!getenv("SAL_CONFIG_WINREG_RETAIN_TMP")) osl::File::remove(aTempFileURL); } ++layer; //TODO: overflow #endif } else if (type == "user") { bool write; if (url.startsWith("!", &url)) { write = true; } else if (url.startsWith("*", &url)) { write = false; } else { write = true; // for backwards compatibility } if (url.isEmpty()) { throw css::uno::RuntimeException( "CONFIGURATION_LAYERS: empty \"user\" URL"); } bool ignore = false; #if ENABLE_DCONF if (write) { OUString token( expand("${SYSUSERCONFIG}/libreoffice/dconfwrite")); osl::DirectoryItem it; osl::FileBase::RC e = osl::DirectoryItem::get(token, it); ignore = e == osl::FileBase::E_None; SAL_INFO( "configmgr", "dconf write (<" << token << "> " << +e << "): " << int(ignore)); if (ignore) { modificationTarget_ = ModificationTarget::Dconf; } } #endif if (!ignore) { if (write) { modificationTarget_ = ModificationTarget::File; modificationFileUrl_ = url; } parseModificationLayer(write ? Data::NO_LAYER : layer, url); } ++layer; //TODO: overflow } else { throw css::uno::RuntimeException( "CONFIGURATION_LAYERS: unknown layer type \"" + type + "\""); } i = n; } } Components::~Components() { // get flag if _exit was already called which is a sign to not secure user config. // this is used for win only currently where calling _exit() unfortunately still // calls destructors (what is not wanted). May be needed for other systems, too // (unknown yet) but can do no harm const bool bExitWasCalled(comphelper::BackupFileHelper::getExitWasCalled()); #ifndef _WIN32 // we can add a SAL_WARN here for other systems where the destructor gets called after // an _exit() call. Still safe - the getExitWasCalled() is used, but a hint that _exit // behaves different on a system SAL_WARN_IF(bExitWasCalled, "configmgr", "Components::~Components() called after _exit() call"); #endif if (bExitWasCalled) { // do not write, re-join threads osl::MutexGuard g(*lock_); if (writeThread_.is()) { writeThread_->join(); } } else { // write changes flushModifications(); } for (auto const& rootElem : roots_) { rootElem->setAlive(false); } } void Components::parseFileLeniently( FileParser * parseFile, OUString const & url, int layer, Partial const * partial, Modifications * modifications, Additions * additions) { assert(parseFile != nullptr); try { (*parseFile)(url, layer, data_, partial, modifications, additions); } catch (const css::container::NoSuchElementException &) { throw; } catch (const css::uno::Exception &) { //TODO: more specific exception catching // Ignore invalid XML files, instead of completely preventing OOo from // starting: TOOLS_WARN_EXCEPTION( "configmgr", "error reading \"" << url << "\""); } } void Components::parseFiles( int layer, OUString const & extension, FileParser * parseFile, OUString const & url, bool recursive) { osl::Directory dir(url); switch (dir.open()) { case osl::FileBase::E_None: break; case osl::FileBase::E_NOENT: if (!recursive) { return; } [[fallthrough]]; default: throw css::uno::RuntimeException( "cannot open directory " + url); } for (;;) { osl::DirectoryItem i; osl::FileBase::RC rc = dir.getNextItem(i, SAL_MAX_UINT32); if (rc == osl::FileBase::E_NOENT) { break; } if (rc != osl::FileBase::E_None) { throw css::uno::RuntimeException( "cannot iterate directory " + url); } osl::FileStatus stat( osl_FileStatus_Mask_Type | osl_FileStatus_Mask_FileName | osl_FileStatus_Mask_FileURL); if (i.getFileStatus(stat) != osl::FileBase::E_None) { throw css::uno::RuntimeException( "cannot stat in directory " + url); } if (stat.getFileType() == osl::FileStatus::Directory) { //TODO: symlinks parseFiles(layer, extension, parseFile, stat.getFileURL(), true); } else { OUString file(stat.getFileName()); if (file.endsWith(extension)) { try { parseFileLeniently( parseFile, stat.getFileURL(), layer, nullptr, nullptr, nullptr); } catch (css::container::NoSuchElementException & e) { if (stat.getFileType() == osl::FileStatus::Link) { SAL_WARN("configmgr", "dangling link <" << stat.getFileURL() << ">"); continue; } throw css::uno::RuntimeException( "stat'ed file does not exist: " + e.Message); } } } } } void Components::parseFileList( int layer, FileParser * parseFile, std::u16string_view urls, bool recordAdditions) { for (sal_Int32 i = 0;;) { OUString url(o3tl::getToken(urls, 0, ' ', i)); if (!url.isEmpty()) { Additions * adds = nullptr; if (recordAdditions) { adds = data_.addExtensionXcuAdditions(url, layer); } try { parseFileLeniently(parseFile, url, layer, nullptr, nullptr, adds); } catch (const css::container::NoSuchElementException &) { TOOLS_WARN_EXCEPTION("configmgr", "file does not exist"); if (adds != nullptr) { data_.removeExtensionXcuAdditions(url); } } } if (i == -1) { break; } } } void Components::parseXcdFiles(int layer, OUString const & url) { osl::Directory dir(url); switch (dir.open()) { case osl::FileBase::E_None: break; case osl::FileBase::E_NOENT: return; default: throw css::uno::RuntimeException( "cannot open directory " + url); } UnresolvedVector unres; std::set< OUString > existingDeps; std::set< OUString > processedDeps; for (;;) { osl::DirectoryItem i; osl::FileBase::RC rc = dir.getNextItem(i, SAL_MAX_UINT32); if (rc == osl::FileBase::E_NOENT) { break; } if (rc != osl::FileBase::E_None) { throw css::uno::RuntimeException( "cannot iterate directory " + url); } osl::FileStatus stat( osl_FileStatus_Mask_Type | osl_FileStatus_Mask_FileName | osl_FileStatus_Mask_FileURL); if (i.getFileStatus(stat) != osl::FileBase::E_None) { throw css::uno::RuntimeException( "cannot stat in directory " + url); } if (stat.getFileType() != osl::FileStatus::Directory) { //TODO: symlinks OUString file(stat.getFileName()); OUString name; if (file.endsWith(".xcd", &name)) { existingDeps.insert(name); rtl::Reference< ParseManager > manager; try { manager = new ParseManager( stat.getFileURL(), new XcdParser(layer, processedDeps, data_)); } catch (css::container::NoSuchElementException & e) { if (stat.getFileType() == osl::FileStatus::Link) { SAL_WARN("configmgr", "dangling link <" << stat.getFileURL() << ">"); continue; } throw css::uno::RuntimeException( "stat'ed file does not exist: " + e.Message); } if (manager->parse(nullptr)) { processedDeps.insert(name); } else { unres.emplace_back(name, manager); } } } } while (!unres.empty()) { bool resolved = false; for (UnresolvedVector::iterator i(unres.begin()); i != unres.end();) { if (i->manager->parse(&existingDeps)) { processedDeps.insert(i->name); i = unres.erase(i); resolved = true; } else { ++i; } } if (!resolved) { throw css::uno::RuntimeException( "xcd: unresolved dependencies in " + url); } } } void Components::parseXcsXcuLayer(int layer, OUString const & url) { parseXcdFiles(layer, url); parseFiles(layer, ".xcs", &parseXcsFile, url + "/schema", false); parseFiles(layer + 1, ".xcu", &parseXcuFile, url + "/data", false); } void Components::parseXcsXcuIniLayer( int layer, OUString const & url, bool recordAdditions) { // Check if ini file exists (otherwise .override would still read global // SCHEMA/DATA variables, which could interfere with unrelated environment // variables): if (rtl::Bootstrap(url).getHandle() == nullptr) return; OUStringBuffer prefix("${.override:"); for (sal_Int32 i = 0; i != url.getLength(); ++i) { sal_Unicode c = url[i]; switch (c) { case '$': case ':': case '\\': prefix.append('\\'); [[fallthrough]]; default: prefix.append(c); } } prefix.append(':'); OUString urls(prefix + "SCHEMA}"); rtl::Bootstrap::expandMacros(urls); if (!urls.isEmpty()) { parseFileList(layer, &parseXcsFile, urls, false); } urls = prefix + "DATA}"; rtl::Bootstrap::expandMacros(urls); if (!urls.isEmpty()) { parseFileList(layer + 1, &parseXcuFile, urls, recordAdditions); } } void Components::parseResLayer(int layer, std::u16string_view url) { OUString resUrl(OUString::Concat(url) + "/res"); parseXcdFiles(layer, resUrl); parseFiles(layer, ".xcu", &parseXcuFile, resUrl, false); } void Components::parseModificationLayer(int layer, OUString const & url) { try { parseFileLeniently(&parseXcuFile, url, layer, nullptr, nullptr, nullptr); } catch (css::container::NoSuchElementException &) { SAL_INFO( "configmgr", "user registrymodifications.xcu does not (yet) exist"); // Migrate old user layer data (can be removed once migration is no // longer relevant, probably OOo 4; also see hack for xsi namespace in // xmlreader::XmlReader::registerNamespaceIri): parseFiles( layer, ".xcu", &parseXcuFile, expand( "${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/user/registry/data"), false); } } int Components::getExtensionLayer(bool shared) const { int layer = shared ? sharedExtensionLayer_ : userExtensionLayer_; if (layer == -1) { throw css::uno::RuntimeException( "insert extension xcs/xcu file into undefined layer"); } return layer; } } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ fice-4-4-0