diff options
Diffstat (limited to 'desktop/source/migration/migration.cxx')
-rw-r--r-- | desktop/source/migration/migration.cxx | 1217 |
1 files changed, 1217 insertions, 0 deletions
diff --git a/desktop/source/migration/migration.cxx b/desktop/source/migration/migration.cxx new file mode 100644 index 0000000000..b0728de61a --- /dev/null +++ b/desktop/source/migration/migration.cxx @@ -0,0 +1,1217 @@ +/* -*- 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 <algorithm> +#include <iterator> +#include <map> +#include <set> + +#include <migration.hxx> +#include "migration_impl.hxx" + +#include <sal/log.hxx> +#include <unotools/textsearch.hxx> +#include <comphelper/processfactory.hxx> +#include <comphelper/sequence.hxx> +#include <unotools/bootstrap.hxx> +#include <rtl/uri.hxx> +#include <i18nlangtag/lang.h> +#include <comphelper/diagnose_ex.hxx> +#include <tools/urlobj.hxx> +#include <officecfg/Office/UI.hxx> +#include <osl/file.hxx> +#include <osl/security.hxx> +#include <unotools/configmgr.hxx> + +#include <com/sun/star/configuration/Update.hpp> +#include <com/sun/star/configuration/theDefaultProvider.hpp> +#include <com/sun/star/container/XNameContainer.hpp> +#include <com/sun/star/task/XJob.hpp> +#include <com/sun/star/beans/NamedValue.hpp> +#include <com/sun/star/beans/XPropertySet.hpp> +#include <com/sun/star/util/XRefreshable.hpp> +#include <com/sun/star/util/XChangesBatch.hpp> +#include <com/sun/star/embed/ElementModes.hpp> +#include <com/sun/star/embed/FileSystemStorageFactory.hpp> +#include <com/sun/star/embed/XStorage.hpp> +#include <com/sun/star/ui/theModuleUIConfigurationManagerSupplier.hpp> +#include <com/sun/star/ui/UIConfigurationManager.hpp> +#include <com/sun/star/ui/XUIConfigurationPersistence.hpp> +#include <vcl/commandinfoprovider.hxx> + +using namespace osl; +using namespace com::sun::star::task; +using namespace com::sun::star::lang; +using namespace com::sun::star::beans; +using namespace com::sun::star::util; +using namespace com::sun::star::container; +using com::sun::star::uno::Exception; +using namespace com::sun::star; + + +namespace desktop +{ + +constexpr OUString ITEM_DESCRIPTOR_COMMANDURL = u"CommandURL"_ustr; +constexpr OUString ITEM_DESCRIPTOR_CONTAINER = u"ItemDescriptorContainer"_ustr; +constexpr OUString ITEM_DESCRIPTOR_LABEL = u"Label"_ustr; + +static OUString mapModuleShortNameToIdentifier(std::u16string_view sShortName) +{ + OUString sIdentifier; + + if ( sShortName == u"StartModule" ) + sIdentifier = "com.sun.star.frame.StartModule"; + + else if ( sShortName == u"swriter" ) + sIdentifier = "com.sun.star.text.TextDocument"; + + else if ( sShortName == u"scalc" ) + sIdentifier = "com.sun.star.sheet.SpreadsheetDocument"; + + else if ( sShortName == u"sdraw" ) + sIdentifier = "com.sun.star.drawing.DrawingDocument"; + + else if ( sShortName == u"simpress" ) + sIdentifier = "com.sun.star.presentation.PresentationDocument"; + + else if ( sShortName == u"smath" ) + sIdentifier = "com.sun.star.formula.FormulaProperties"; + + else if ( sShortName == u"schart" ) + sIdentifier = "com.sun.star.chart2.ChartDocument"; + + else if ( sShortName == u"BasicIDE" ) + sIdentifier = "com.sun.star.script.BasicIDE"; + + else if ( sShortName == u"dbapp" ) + sIdentifier = "com.sun.star.sdb.OfficeDatabaseDocument"; + + else if ( sShortName == u"sglobal" ) + sIdentifier = "com.sun.star.text.GlobalDocument"; + + else if ( sShortName == u"sweb" ) + sIdentifier = "com.sun.star.text.WebDocument"; + + else if ( sShortName == u"swxform" ) + sIdentifier = "com.sun.star.xforms.XMLFormDocument"; + + else if ( sShortName == u"sbibliography" ) + sIdentifier = "com.sun.star.frame.Bibliography"; + + return sIdentifier; +} + +bool MigrationImpl::alreadyMigrated() +{ + OUString aStr = m_aInfo.userdata + "/MIGRATED4"; + File aFile(aStr); + // create migration stamp, and/or check its existence + bool bRet = aFile.open (osl_File_OpenFlag_Write | osl_File_OpenFlag_Create | osl_File_OpenFlag_NoLock) == FileBase::E_EXIST; + SAL_INFO( "desktop.migration", "File '" << aStr << "' exists? " << bRet ); + return bRet; +} + +bool MigrationImpl::initializeMigration() +{ + bool bRet = false; + + if (!checkMigrationCompleted()) { + readAvailableMigrations(m_vMigrationsAvailable); + sal_Int32 nIndex = findPreferredMigrationProcess(m_vMigrationsAvailable); + // m_aInfo is now set to the preferred migration source + if ( nIndex >= 0 ) { + if (alreadyMigrated()) + return false; + m_vrMigrations = readMigrationSteps(m_vMigrationsAvailable[nIndex].name); + } + + bRet = !m_aInfo.userdata.isEmpty(); + } + + SAL_INFO( "desktop.migration", "Migration " << ( bRet ? "needed" : "not required" ) ); + + return bRet; +} + +void Migration::migrateSettingsIfNecessary() +{ + MigrationImpl aImpl; + + if (! aImpl.initializeMigration() ) + return; + + bool bResult = false; + try { + bResult = aImpl.doMigration(); + } catch (const Exception&) { + TOOLS_WARN_EXCEPTION( "desktop", "doMigration()"); + } + OSL_ENSURE(bResult, "Migration has not been successful"); +} + +MigrationImpl::MigrationImpl() +{ +} + +MigrationImpl::~MigrationImpl() +{ +} + +// The main entry point for migrating settings +bool MigrationImpl::doMigration() +{ + // compile file list for migration + m_vrFileList = compileFileList(); + + bool result = false; + try { + NewVersionUIInfo aNewVersionUIInfo; + std::vector< MigrationModuleInfo > vModulesInfo = detectUIChangesForAllModules(); + aNewVersionUIInfo.init(vModulesInfo); + + copyFiles(); + + static constexpr OUString sMenubarResourceURL(u"private:resource/menubar/menubar"_ustr); + static constexpr OUStringLiteral sToolbarResourcePre(u"private:resource/toolbar/"); + for (MigrationModuleInfo & i : vModulesInfo) { + OUString sModuleIdentifier = mapModuleShortNameToIdentifier(i.sModuleShortName); + if (sModuleIdentifier.isEmpty()) + continue; + + + OUString aOldCfgDataPath = m_aInfo.userdata + "/user/config/soffice.cfg/modules/" + i.sModuleShortName; + uno::Sequence< uno::Any > lArgs {uno::Any(aOldCfgDataPath), uno::Any(embed::ElementModes::READ)}; + + uno::Reference< uno::XComponentContext > xContext(comphelper::getProcessComponentContext()); + uno::Reference< lang::XSingleServiceFactory > xStorageFactory(embed::FileSystemStorageFactory::create(xContext)); + uno::Reference< embed::XStorage > xModules(xStorageFactory->createInstanceWithArguments(lArgs), uno::UNO_QUERY); + uno::Reference< ui::XUIConfigurationManager2 > xOldCfgManager = ui::UIConfigurationManager::create(xContext); + + if ( xModules.is() ) { + xOldCfgManager->setStorage( xModules ); + xOldCfgManager->reload(); + } + + uno::Reference< ui::XUIConfigurationManager > xCfgManager = aNewVersionUIInfo.getConfigManager(i.sModuleShortName); + + if (i.bHasMenubar) { + uno::Reference< container::XIndexContainer > xOldVersionMenuSettings(xOldCfgManager->getSettings(sMenubarResourceURL, true), uno::UNO_QUERY); + uno::Reference< container::XIndexContainer > xNewVersionMenuSettings = aNewVersionUIInfo.getNewMenubarSettings(i.sModuleShortName); + compareOldAndNewConfig(OUString(), xOldVersionMenuSettings, xNewVersionMenuSettings, sMenubarResourceURL); + mergeOldToNewVersion(xCfgManager, xNewVersionMenuSettings, sModuleIdentifier, sMenubarResourceURL); + } + + sal_Int32 nToolbars = i.m_vToolbars.size(); + if (nToolbars >0) { + for (sal_Int32 j=0; j<nToolbars; ++j) { + OUString sToolbarName = i.m_vToolbars[j]; + OUString sToolbarResourceURL = sToolbarResourcePre + sToolbarName; + + uno::Reference< container::XIndexContainer > xOldVersionToolbarSettings(xOldCfgManager->getSettings(sToolbarResourceURL, true), uno::UNO_QUERY); + uno::Reference< container::XIndexContainer > xNewVersionToolbarSettings = aNewVersionUIInfo.getNewToolbarSettings(i.sModuleShortName, sToolbarName); + compareOldAndNewConfig(OUString(), xOldVersionToolbarSettings, xNewVersionToolbarSettings, sToolbarResourceURL); + mergeOldToNewVersion(xCfgManager, xNewVersionToolbarSettings, sModuleIdentifier, sToolbarResourceURL); + } + } + + m_aOldVersionItemsHashMap.clear(); + } + + // execute the migration items from Setup.xcu + copyConfig(); + + // execute custom migration services from Setup.xcu + // and refresh the cache + runServices(); + uno::Reference< XRefreshable >( + configuration::theDefaultProvider::get(comphelper::getProcessComponentContext()), + uno::UNO_QUERY_THROW)->refresh(); + + result = true; + } catch (const css::uno::Exception &) { + TOOLS_WARN_EXCEPTION( + "desktop.migration", + "ignored Exception while migrating from version \"" << m_aInfo.productname + << "\" data \"" << m_aInfo.userdata << "\""); + } + + // prevent running the migration multiple times + setMigrationCompleted(); + return result; +} + +void MigrationImpl::setMigrationCompleted() +{ + try { + uno::Reference< XPropertySet > aPropertySet(getConfigAccess("org.openoffice.Setup/Office", true), uno::UNO_QUERY_THROW); + aPropertySet->setPropertyValue("MigrationCompleted", uno::Any(true)); + uno::Reference< XChangesBatch >(aPropertySet, uno::UNO_QUERY_THROW)->commitChanges(); + } catch (...) { + // fail silently + } +} + +bool MigrationImpl::checkMigrationCompleted() +{ + bool bMigrationCompleted = false; + try { + uno::Reference< XPropertySet > aPropertySet( + getConfigAccess("org.openoffice.Setup/Office"), uno::UNO_QUERY_THROW); + aPropertySet->getPropertyValue("MigrationCompleted") >>= bMigrationCompleted; + + if( !bMigrationCompleted && getenv("SAL_DISABLE_USERMIGRATION" ) ) { + // migration prevented - fake its success + setMigrationCompleted(); + bMigrationCompleted = true; + } + } catch (const Exception&) { + // just return false... + } + SAL_INFO( "desktop.migration", "Migration " << ( bMigrationCompleted ? "already completed" : "not done" ) ); + + return bMigrationCompleted; +} + +static void insertSorted(migrations_available& rAvailableMigrations, supported_migration const & aSupportedMigration) +{ + migrations_available::iterator pIter = std::find_if(rAvailableMigrations.begin(), rAvailableMigrations.end(), + [&aSupportedMigration](const supported_migration& rMigration) { return rMigration.nPriority < aSupportedMigration.nPriority; }); + if (pIter != rAvailableMigrations.end()) + rAvailableMigrations.insert(pIter, aSupportedMigration ); + else + rAvailableMigrations.push_back( aSupportedMigration ); +} + +void MigrationImpl::readAvailableMigrations(migrations_available& rAvailableMigrations) +{ + // get supported version names + uno::Reference< XNameAccess > aMigrationAccess(getConfigAccess("org.openoffice.Setup/Migration/SupportedVersions"), uno::UNO_SET_THROW); + const uno::Sequence< OUString > seqSupportedVersions = aMigrationAccess->getElementNames(); + + static constexpr OUStringLiteral aVersionIdentifiers( u"VersionIdentifiers" ); + static constexpr OUStringLiteral aPriorityIdentifier( u"Priority" ); + + for (OUString const & supportedVersion :seqSupportedVersions) { + sal_Int32 nPriority( 0 ); + uno::Sequence< OUString > seqVersions; + uno::Reference< XNameAccess > xMigrationData( aMigrationAccess->getByName(supportedVersion), uno::UNO_QUERY_THROW ); + xMigrationData->getByName( aVersionIdentifiers ) >>= seqVersions; + xMigrationData->getByName( aPriorityIdentifier ) >>= nPriority; + + supported_migration aSupportedMigration; + aSupportedMigration.name = supportedVersion; + aSupportedMigration.nPriority = nPriority; + for (OUString const & s : std::as_const(seqVersions)) + aSupportedMigration.supported_versions.push_back(s.trim()); + insertSorted( rAvailableMigrations, aSupportedMigration ); + SAL_INFO( "desktop.migration", " available migration '" << aSupportedMigration.name << "'" ); + } +} + +migrations_vr MigrationImpl::readMigrationSteps(const OUString& rMigrationName) +{ + // get migration access + uno::Reference< XNameAccess > aMigrationAccess(getConfigAccess("org.openoffice.Setup/Migration/SupportedVersions"), uno::UNO_SET_THROW); + uno::Reference< XNameAccess > xMigrationData( aMigrationAccess->getByName(rMigrationName), uno::UNO_QUERY_THROW ); + + // get migration description from org.openoffice.Setup/Migration + // and build vector of migration steps + uno::Reference< XNameAccess > theNameAccess(xMigrationData->getByName("MigrationSteps"), uno::UNO_QUERY_THROW); + uno::Reference< XNameAccess > tmpAccess; + uno::Sequence< OUString > tmpSeq; + migrations_vr vrMigrations(new migrations_v); + const css::uno::Sequence<OUString> aMigrationSteps = theNameAccess->getElementNames(); + for (const OUString& rMigrationStep : aMigrationSteps) { + // get current migration step + theNameAccess->getByName(rMigrationStep) >>= tmpAccess; + migration_step tmpStep; + + // read included files from current step description + if (tmpAccess->getByName("IncludedFiles") >>= tmpSeq) { + for (const OUString& rSeqEntry : std::as_const(tmpSeq)) + tmpStep.includeFiles.push_back(rSeqEntry); + } + + // excluded files... + if (tmpAccess->getByName("ExcludedFiles") >>= tmpSeq) { + for (const OUString& rSeqEntry : std::as_const(tmpSeq)) + tmpStep.excludeFiles.push_back(rSeqEntry); + } + + // included nodes... + if (tmpAccess->getByName("IncludedNodes") >>= tmpSeq) { + for (const OUString& rSeqEntry : std::as_const(tmpSeq)) + tmpStep.includeConfig.push_back(rSeqEntry); + } + + // excluded nodes... + if (tmpAccess->getByName("ExcludedNodes") >>= tmpSeq) { + for (const OUString& rSeqEntry : std::as_const(tmpSeq)) + tmpStep.excludeConfig.push_back(rSeqEntry); + } + + // excluded extensions... + if (tmpAccess->getByName("ExcludedExtensions") >>= tmpSeq) { + for (const OUString& rSeqEntry : std::as_const(tmpSeq)) + tmpStep.excludeExtensions.push_back(rSeqEntry); + } + + // generic service + tmpAccess->getByName("MigrationService") >>= tmpStep.service; + + vrMigrations->push_back(tmpStep); + } + return vrMigrations; +} + +static FileBase::RC _checkAndCreateDirectory(INetURLObject const & dirURL) +{ + FileBase::RC result = Directory::create(dirURL.GetMainURL(INetURLObject::DecodeMechanism::ToIUri)); + if (result == FileBase::E_NOENT) { + INetURLObject baseURL(dirURL); + baseURL.removeSegment(); + _checkAndCreateDirectory(baseURL); + return Directory::create(dirURL.GetMainURL(INetURLObject::DecodeMechanism::ToIUri)); + } else + return result; +} + +#if defined UNX && ! defined MACOSX + +const char XDG_CONFIG_PART[] = "/.config/"; + +OUString MigrationImpl::preXDGConfigDir(const OUString& rConfigDir) +{ + OUString aPreXDGConfigPath; + const char* pXDGCfgHome = getenv("XDG_CONFIG_HOME"); + + // cater for XDG_CONFIG_HOME change + // If XDG_CONFIG_HOME is set then we; + // assume the user knows what they are doing ( room for improvement here, we could + // of course search the default config dir etc. also - but this is more complex, + // we would need to weigh results from the current config dir against matches in + // the 'old' config dir etc. ) - currently we just use the returned config dir. + // If XDG_CONFIG_HOME is NOT set; + // assume then we should now using the default $HOME/.config config location for + // our user profiles, however *all* previous libreoffice and openoffice.org + // configurations will be in the 'old' config directory and that's where we need + // to search - we convert the returned config dir to the 'old' dir + if ( !pXDGCfgHome && rConfigDir.endsWith( XDG_CONFIG_PART ) ) + // remove trailing '.config/' but leave the terminating '/' + aPreXDGConfigPath = rConfigDir.copy( 0, rConfigDir.getLength() - sizeof( XDG_CONFIG_PART ) + 2 ); + else + aPreXDGConfigPath = rConfigDir; + + // the application-specific config dir is no longer prefixed by '.' because it is hidden under ".config" + // we have to add the '.' for the pre-XDG directory names + aPreXDGConfigPath += "."; + + return aPreXDGConfigPath; +} +#endif + +void MigrationImpl::setInstallInfoIfExist( + install_info& aInfo, + std::u16string_view rConfigDir, + const OUString& rVersion) +{ + OUString url(INetURLObject(rConfigDir).GetMainURL(INetURLObject::DecodeMechanism::NONE)); + osl::DirectoryItem item; + osl::FileStatus stat(osl_FileStatus_Mask_Type); + + if (osl::DirectoryItem::get(url, item) == osl::FileBase::E_None + && item.getFileStatus(stat) == osl::FileBase::E_None + && stat.getFileType() == osl::FileStatus::Directory) { + aInfo.userdata = url; + aInfo.productname = rVersion; + } +} + +install_info MigrationImpl::findInstallation(const strings_v& rVersions) +{ + + OUString aTopConfigDir; + osl::Security().getConfigDir( aTopConfigDir ); + if ( !aTopConfigDir.isEmpty() && aTopConfigDir[ aTopConfigDir.getLength()-1 ] != '/' ) + aTopConfigDir += "/"; + +#if defined UNX && ! defined MACOSX + OUString aPreXDGTopConfigDir = preXDGConfigDir(aTopConfigDir); +#endif + + install_info aInfo; + for (auto const& elem : rVersions) + { + OUString aVersion, aProfileName; + sal_Int32 nSeparatorIndex = elem.indexOf('='); + if ( nSeparatorIndex != -1 ) { + aVersion = elem.copy( 0, nSeparatorIndex ); + aProfileName = elem.copy( nSeparatorIndex+1 ); + } + + if ( !aVersion.isEmpty() && !aProfileName.isEmpty() && + ( aInfo.userdata.isEmpty() || + aProfileName.equalsIgnoreAsciiCase( + utl::ConfigManager::getProductName() ) ) ) { + setInstallInfoIfExist(aInfo, Concat2View(aTopConfigDir + aProfileName), aVersion); +#if defined UNX && ! defined MACOSX + //try preXDG path if the new one does not exist + if ( aInfo.userdata.isEmpty()) + setInstallInfoIfExist(aInfo, Concat2View(aPreXDGTopConfigDir + aProfileName), aVersion); +#endif + } + } + + return aInfo; +} + +sal_Int32 MigrationImpl::findPreferredMigrationProcess(const migrations_available& rAvailableMigrations) +{ + sal_Int32 nIndex( -1 ); + sal_Int32 i( 0 ); + + for (auto const& availableMigration : rAvailableMigrations) + { + install_info aInstallInfo = findInstallation(availableMigration.supported_versions); + if (!aInstallInfo.productname.isEmpty() ) { + m_aInfo = aInstallInfo; + nIndex = i; + break; + } + ++i; + } + + SAL_INFO( "desktop.migration", " preferred migration is from product '" << m_aInfo.productname << "'"); + SAL_INFO( "desktop.migration", " and settings directory '" << m_aInfo.userdata << "'"); + + return nIndex; +} + +strings_vr MigrationImpl::applyPatterns(const strings_v& vSet, const strings_v& vPatterns) +{ + using namespace utl; + strings_vr vrResult(new strings_v); + for (auto const& pattern : vPatterns) + { + // find matches for this pattern in input set + // and copy them to the result + SearchParam param(pattern, SearchParam::SearchType::Regexp); + TextSearch ts(param, LANGUAGE_DONTKNOW); + sal_Int32 start = 0; + sal_Int32 end = 0; + for (auto const& elem : vSet) + { + end = elem.getLength(); + if (ts.SearchForward(elem, &start, &end)) + vrResult->push_back(elem); + } + } + return vrResult; +} + +strings_vr MigrationImpl::getAllFiles(const OUString& baseURL) const +{ + strings_vr vrResult(new strings_v); + + // get sub dirs + Directory dir(baseURL); + if (dir.open() == FileBase::E_None) { + strings_v vSubDirs; + strings_vr vrSubResult; + + // work through directory contents... + DirectoryItem item; + FileStatus fs(osl_FileStatus_Mask_Type | osl_FileStatus_Mask_FileURL); + while (dir.getNextItem(item) == FileBase::E_None) { + if (item.getFileStatus(fs) == FileBase::E_None) { + if (fs.getFileType() == FileStatus::Directory) + vSubDirs.push_back(fs.getFileURL()); + else + vrResult->push_back(fs.getFileURL()); + } + } + + // recurse subfolders + for (auto const& subDir : vSubDirs) + { + vrSubResult = getAllFiles(subDir); + vrResult->insert(vrResult->end(), vrSubResult->begin(), vrSubResult->end()); + } + } + return vrResult; +} + +namespace +{ + +// removes elements of vector 2 in vector 1 +strings_v subtract(strings_v && a, strings_v && b) +{ + std::sort(a.begin(), a.end()); + strings_v::iterator ae(std::unique(a.begin(), a.end())); + std::sort(b.begin(), b.end()); + strings_v::iterator be(std::unique(b.begin(), b.end())); + strings_v c; + std::set_difference(a.begin(), ae, b.begin(), be, std::back_inserter(c)); + return c; +} + +} + +strings_vr MigrationImpl::compileFileList() +{ + + strings_vr vrResult(new strings_v); + + // get a list of all files: + strings_vr vrFiles = getAllFiles(m_aInfo.userdata); + + // get a file list result for each migration step + for (auto const& rMigration : *m_vrMigrations) + { + strings_vr vrInclude = applyPatterns(*vrFiles, rMigration.includeFiles); + strings_vr vrExclude = applyPatterns(*vrFiles, rMigration.excludeFiles); + strings_v sub(subtract(std::move(*vrInclude), std::move(*vrExclude))); + vrResult->insert(vrResult->end(), sub.begin(), sub.end()); + } + return vrResult; +} + +namespace +{ + +struct componentParts { + std::set< OUString > includedPaths; + std::set< OUString > excludedPaths; +}; + +typedef std::map< OUString, componentParts > Components; + +bool getComponent(OUString const & path, OUString * component) +{ + OSL_ASSERT(component != nullptr); + if (path.isEmpty() || path[0] != '/') { + SAL_INFO( "desktop.migration", "configuration migration in/exclude path " << path << " ignored (does not start with slash)" ); + return false; + } + sal_Int32 i = path.indexOf('/', 1); + *component = i < 0 ? path.copy(1) : path.copy(1, i - 1); + return true; +} + +void renameMigratedSetElementTo( + css::uno::Reference<css::container::XNameContainer> const & set, OUString const & currentName, + OUString const & migratedName) +{ + // To avoid unexpected data loss, the code is careful to only rename from currentName to + // migratedName in the expected case where the currentName element exists and the migratedName + // element doesn't exist: + bool const hasCurrent = set->hasByName(currentName); + bool const hasMigrated = set->hasByName(migratedName); + if (hasCurrent && !hasMigrated) { + auto const elem = set->getByName(currentName); + set->removeByName(currentName); + set->insertByName(migratedName, elem); + } else { + SAL_INFO_IF(!hasCurrent, "desktop.migration", "unexpectedly missing " << currentName); + SAL_INFO_IF(hasMigrated, "desktop.migration", "unexpectedly present " << migratedName); + } +} + +void renameMigratedSetElementBack( + css::uno::Reference<css::container::XNameContainer> const & set, OUString const & currentName, + OUString const & migratedName) +{ + // To avoid unexpected data loss, the code is careful to ensure that in the end a currentName + // element exists, creating it from a template if the migratedName element had unexpectedly gone + // missing: + bool const hasMigrated = set->hasByName(migratedName); + css::uno::Any elem; + if (hasMigrated) { + elem = set->getByName(migratedName); + set->removeByName(migratedName); + } else { + SAL_INFO("desktop.migration", "unexpected loss of " << migratedName); + elem <<= css::uno::Reference<css::lang::XSingleServiceFactory>( + set, css::uno::UNO_QUERY_THROW)->createInstance(); + } + if (set->hasByName(currentName)) { + SAL_INFO("desktop.migration", "unexpected reappearance of " << currentName); + if (hasMigrated) { + SAL_INFO( + "desktop.migration", + "reappeared " << currentName << " overwritten with " << migratedName); + set->replaceByName(currentName, elem); + } + } else { + set->insertByName(currentName, elem); + } +} + +} + +void MigrationImpl::copyConfig() +{ + Components comps; + for (auto const& rMigrationStep : *m_vrMigrations) { + for (const OUString& rIncludePath : rMigrationStep.includeConfig) { + OUString comp; + if (getComponent(rIncludePath, &comp)) { + comps[comp].includedPaths.insert(rIncludePath); + } + } + for (const OUString& rExcludePath : rMigrationStep.excludeConfig) { + OUString comp; + if (getComponent(rExcludePath, &comp)) { + comps[comp].excludedPaths.insert(rExcludePath); + } + } + } + + // check if the shared registrymodifications.xcu file exists + bool bRegistryModificationsXcuExists = false; + OUString regFilePath = m_aInfo.userdata + "/user/registrymodifications.xcu"; + File regFile(regFilePath); + ::osl::FileBase::RC nError = regFile.open(osl_File_OpenFlag_Read); + if ( nError == ::osl::FileBase::E_None ) { + bRegistryModificationsXcuExists = true; + regFile.close(); + } + + // If the to-be-migrated data contains modifications of + // /org.openoffice.Office.UI/ColorScheme/ColorSchemes set elements named after the migrated + // product name, those modifications must instead be made to the corresponding set elements + // named after the current product name. However, if the current configuration data does not + // contain those old-named set elements at all, their modification data would silently be + // ignored by css.configuration.XUpdate::insertModificationXcuFile. So temporarily rename any + // new-named set elements to their old-named counterparts here, and rename them back again down + // below after importing the migrated data: + OUString sProductName = utl::ConfigManager::getProductName(); + OUString sProductNameDark = sProductName + " Dark"; + OUString sMigratedProductName = m_aInfo.productname; + // remove version number from the end of product name if there’s one + if (isdigit(sMigratedProductName[sMigratedProductName.getLength() - 1])) + sMigratedProductName = (sMigratedProductName.copy(0, m_aInfo.productname.getLength() - 1)).trim(); + OUString sMigratedProductNameDark = sMigratedProductName + " Dark"; + auto const tempRename = sMigratedProductName != sProductName; + if (tempRename) { + auto const batch = comphelper::ConfigurationChanges::create(); + auto const schemes = officecfg::Office::UI::ColorScheme::ColorSchemes::get(batch); + renameMigratedSetElementTo(schemes, sProductName, sMigratedProductName); + renameMigratedSetElementTo(schemes, sProductNameDark, sMigratedProductNameDark); + batch->commit(); + } + + for (auto const& comp : comps) + { + if (!comp.second.includedPaths.empty()) { + if (!bRegistryModificationsXcuExists) { + // shared registrymodifications.xcu does not exists + // the configuration is split in many registry files + // determine the file names from the first element in included paths + OUStringBuffer buf(m_aInfo.userdata + + "/user/registry/data"); + sal_Int32 n = 0; + do { + OUString seg(comp.first.getToken(0, '.', n)); + OUString enc( + rtl::Uri::encode( + seg, rtl_UriCharClassPchar, rtl_UriEncodeStrict, + RTL_TEXTENCODING_UTF8)); + if (enc.isEmpty() && !seg.isEmpty()) { + SAL_INFO( "desktop.migration", "configuration migration component " << comp.first << " ignored (cannot be encoded as file path)" ); + goto next; + } + buf.append("/" + enc); + } while (n >= 0); + buf.append(".xcu"); + regFilePath = buf.makeStringAndClear(); + } + configuration::Update::get( + comphelper::getProcessComponentContext())-> + insertModificationXcuFile( + regFilePath, + comphelper::containerToSequence(comp.second.includedPaths), + comphelper::containerToSequence(comp.second.excludedPaths)); + + } else { + SAL_INFO( "desktop.migration", "configuration migration component " << comp.first << " ignored (only excludes, no includes)" ); + } +next: + ; + } + if (tempRename) { + auto const batch = comphelper::ConfigurationChanges::create(); + auto const schemes = officecfg::Office::UI::ColorScheme::ColorSchemes::get(batch); + renameMigratedSetElementBack(schemes, sProductName, sMigratedProductName); + renameMigratedSetElementBack(schemes, sProductNameDark, sMigratedProductNameDark); + batch->commit(); + } + // checking the migrated (product name related) color scheme name, and replace it to the current version scheme name + try + { + OUString sMigratedColorScheme; + uno::Reference<XPropertySet> aPropertySet( + getConfigAccess("org.openoffice.Office.UI/ColorScheme", true), uno::UNO_QUERY_THROW); + if (aPropertySet->getPropertyValue("CurrentColorScheme") >>= sMigratedColorScheme) + { + if (sMigratedColorScheme.equals(sMigratedProductName)) + { + aPropertySet->setPropertyValue("CurrentColorScheme", + uno::Any(sProductName)); + uno::Reference<XChangesBatch>(aPropertySet, uno::UNO_QUERY_THROW)->commitChanges(); + } + else if (sMigratedColorScheme.equals(sMigratedProductNameDark)) + { + aPropertySet->setPropertyValue("CurrentColorScheme", + uno::Any(sProductNameDark)); + uno::Reference<XChangesBatch>(aPropertySet, uno::UNO_QUERY_THROW)->commitChanges(); + } + } + } catch (const Exception&) { + // fail silently... + } +} + +uno::Reference< XNameAccess > MigrationImpl::getConfigAccess(const char* pPath, bool bUpdate) +{ + uno::Reference< XNameAccess > xNameAccess; + try { + OUString sAccessSrvc; + if (bUpdate) + sAccessSrvc = "com.sun.star.configuration.ConfigurationUpdateAccess"; + else + sAccessSrvc = "com.sun.star.configuration.ConfigurationAccess"; + + OUString sConfigURL = OUString::createFromAscii(pPath); + + uno::Reference< XMultiServiceFactory > theConfigProvider( + configuration::theDefaultProvider::get( + comphelper::getProcessComponentContext())); + + // access the provider + uno::Sequence< uno::Any > theArgs {uno::Any(sConfigURL)}; + xNameAccess.set( + theConfigProvider->createInstanceWithArguments( + sAccessSrvc, theArgs ), uno::UNO_QUERY_THROW ); + } catch (const css::uno::Exception&) { + TOOLS_WARN_EXCEPTION("desktop.migration", "ignoring"); + } + return xNameAccess; +} + +void MigrationImpl::copyFiles() +{ + OUString localName; + OUString destName; + OUString userInstall; + utl::Bootstrap::PathStatus aStatus; + aStatus = utl::Bootstrap::locateUserInstallation(userInstall); + if (aStatus == utl::Bootstrap::PATH_EXISTS) { + for (auto const& rFile : *m_vrFileList) + { + // remove installation prefix from file + localName = rFile.copy(m_aInfo.userdata.getLength()); + if (localName.endsWith( "/autocorr/acor_.dat")) { + // Previous versions used an empty language tag for + // LANGUAGE_DONTKNOW with the "[All]" autocorrection entry. + // As of LibreOffice 4.0 it is 'und' for LANGUAGE_UNDETERMINED + // so the file name is "acor_und.dat". + localName = OUString::Concat(localName.subView( 0, localName.getLength() - 4)) + "und.dat"; + } + destName = userInstall + localName; + INetURLObject aURL(destName); + // check whether destination directory exists + aURL.removeSegment(); + _checkAndCreateDirectory(aURL); + FileBase::RC copyResult = File::copy(rFile, destName); + if (copyResult != FileBase::E_None) { + SAL_WARN( "desktop", "Cannot copy " << rFile << " to " << destName); + } + } + } else { + OSL_FAIL("copyFiles: UserInstall does not exist"); + } +} + +void MigrationImpl::runServices() +{ + // Build argument array + uno::Sequence< uno::Any > seqArguments(3); + auto pseqArguments = seqArguments.getArray(); + pseqArguments[0] <<= NamedValue("Productname", + uno::Any(m_aInfo.productname)); + pseqArguments[1] <<= NamedValue("UserData", + uno::Any(m_aInfo.userdata)); + + + // create an instance of every migration service + // and execute the migration job + uno::Reference< XJob > xMigrationJob; + + uno::Reference< uno::XComponentContext > xContext(comphelper::getProcessComponentContext()); + for (auto const& rMigration : *m_vrMigrations) + { + if( !rMigration.service.isEmpty()) { + + try { + // set black list for extension migration + uno::Sequence< OUString > seqExtDenyList; + sal_uInt32 nSize = rMigration.excludeExtensions.size(); + if ( nSize > 0 ) + seqExtDenyList = comphelper::arrayToSequence< OUString >( + rMigration.excludeExtensions.data(), nSize ); + pseqArguments[2] <<= NamedValue("ExtensionDenyList", + uno::Any( seqExtDenyList )); + + xMigrationJob.set( + xContext->getServiceManager()->createInstanceWithArgumentsAndContext(rMigration.service, seqArguments, xContext), + uno::UNO_QUERY_THROW); + + xMigrationJob->execute(uno::Sequence< NamedValue >()); + + + } catch (const Exception&) { + TOOLS_WARN_EXCEPTION( "desktop", "Execution of migration service failed. Service: " + << rMigration.service); + } catch (...) { + SAL_WARN( "desktop", "Execution of migration service failed (Exception caught).\nService: " + << rMigration.service << "\nNo message available"); + } + + } + } +} + +std::vector< MigrationModuleInfo > MigrationImpl::detectUIChangesForAllModules() const +{ + std::vector< MigrationModuleInfo > vModulesInfo; + static constexpr OUStringLiteral MENUBAR(u"menubar"); + static constexpr OUStringLiteral TOOLBAR(u"toolbar"); + + uno::Sequence< uno::Any > lArgs {uno::Any(m_aInfo.userdata + "/user/config/soffice.cfg/modules"), + uno::Any(embed::ElementModes::READ)}; + + uno::Reference< lang::XSingleServiceFactory > xStorageFactory( + embed::FileSystemStorageFactory::create(comphelper::getProcessComponentContext())); + uno::Reference< embed::XStorage > xModules; + + xModules.set(xStorageFactory->createInstanceWithArguments(lArgs), uno::UNO_QUERY); + if (!xModules.is()) + return vModulesInfo; + + uno::Sequence< OUString > lNames = xModules->getElementNames(); + sal_Int32 nLength = lNames.getLength(); + for (sal_Int32 i=0; i<nLength; ++i) { + OUString sModuleShortName = lNames[i]; + uno::Reference< embed::XStorage > xModule = xModules->openStorageElement(sModuleShortName, embed::ElementModes::READ); + if (xModule.is()) { + MigrationModuleInfo aModuleInfo; + + uno::Reference< embed::XStorage > xMenubar = xModule->openStorageElement(MENUBAR, embed::ElementModes::READ); + if (xMenubar.is()) { + if (xMenubar->getElementNames().hasElements()) { + aModuleInfo.sModuleShortName = sModuleShortName; + aModuleInfo.bHasMenubar = true; + } + } + + uno::Reference< embed::XStorage > xToolbar = xModule->openStorageElement(TOOLBAR, embed::ElementModes::READ); + if (xToolbar.is()) { + const ::uno::Sequence< OUString > lToolbars = xToolbar->getElementNames(); + for (OUString const & sToolbarName : lToolbars) { + if (sToolbarName.startsWith("custom_")) + continue; + + aModuleInfo.sModuleShortName = sModuleShortName; + sal_Int32 nIndex = sToolbarName.lastIndexOf('.'); + if (nIndex > 0) { + std::u16string_view sExtension(sToolbarName.subView(nIndex)); + OUString sToolbarResourceName(sToolbarName.copy(0, nIndex)); + if (!sToolbarResourceName.isEmpty() && sExtension == u".xml") + aModuleInfo.m_vToolbars.push_back(sToolbarResourceName); + } + } + } + + if (!aModuleInfo.sModuleShortName.isEmpty()) + vModulesInfo.push_back(aModuleInfo); + } + } + + return vModulesInfo; +} + +void MigrationImpl::compareOldAndNewConfig(const OUString& sParent, + const uno::Reference< container::XIndexContainer >& xIndexOld, + const uno::Reference< container::XIndexContainer >& xIndexNew, + const OUString& sResourceURL) +{ + static constexpr OUStringLiteral MENU_SEPARATOR(u" | "); + + std::vector< MigrationItem > vOldItems; + std::vector< MigrationItem > vNewItems; + uno::Sequence< beans::PropertyValue > aProps; + sal_Int32 nOldCount = xIndexOld->getCount(); + sal_Int32 nNewCount = xIndexNew->getCount(); + + for (int n=0; n<nOldCount; ++n) { + MigrationItem aMigrationItem; + if (xIndexOld->getByIndex(n) >>= aProps) { + for(beans::PropertyValue const & prop : std::as_const(aProps)) { + if ( prop.Name == ITEM_DESCRIPTOR_COMMANDURL ) + prop.Value >>= aMigrationItem.m_sCommandURL; + else if ( prop.Name == ITEM_DESCRIPTOR_CONTAINER ) + prop.Value >>= aMigrationItem.m_xPopupMenu; + } + + if (!aMigrationItem.m_sCommandURL.isEmpty()) + vOldItems.push_back(aMigrationItem); + } + } + + for (int n=0; n<nNewCount; ++n) { + MigrationItem aMigrationItem; + if (xIndexNew->getByIndex(n) >>= aProps) { + for(beans::PropertyValue const & prop : std::as_const(aProps)) { + if ( prop.Name == ITEM_DESCRIPTOR_COMMANDURL ) + prop.Value >>= aMigrationItem.m_sCommandURL; + else if ( prop.Name == ITEM_DESCRIPTOR_CONTAINER ) + prop.Value >>= aMigrationItem.m_xPopupMenu; + } + + if (!aMigrationItem.m_sCommandURL.isEmpty()) + vNewItems.push_back(aMigrationItem); + } + } + + OUString sSibling; + for (auto const& oldItem : vOldItems) + { + std::vector< MigrationItem >::iterator pFound = std::find(vNewItems.begin(), vNewItems.end(), oldItem); + if (pFound != vNewItems.end() && oldItem.m_xPopupMenu.is()) { + OUString sName; + if (!sParent.isEmpty()) + sName = sParent + MENU_SEPARATOR + oldItem.m_sCommandURL; + else + sName = oldItem.m_sCommandURL; + compareOldAndNewConfig(sName, oldItem.m_xPopupMenu, pFound->m_xPopupMenu, sResourceURL); + } else if (pFound == vNewItems.end()) { + MigrationItem aMigrationItem(sParent, sSibling, oldItem.m_sCommandURL, oldItem.m_xPopupMenu); + if (m_aOldVersionItemsHashMap.find(sResourceURL)==m_aOldVersionItemsHashMap.end()) { + std::vector< MigrationItem > vMigrationItems; + m_aOldVersionItemsHashMap.emplace(sResourceURL, vMigrationItems); + m_aOldVersionItemsHashMap[sResourceURL].push_back(aMigrationItem); + } else { + if (std::find(m_aOldVersionItemsHashMap[sResourceURL].begin(), m_aOldVersionItemsHashMap[sResourceURL].end(), aMigrationItem)==m_aOldVersionItemsHashMap[sResourceURL].end()) + m_aOldVersionItemsHashMap[sResourceURL].push_back(aMigrationItem); + } + } + + sSibling = oldItem.m_sCommandURL; + } +} + +void MigrationImpl::mergeOldToNewVersion(const uno::Reference< ui::XUIConfigurationManager >& xCfgManager, + const uno::Reference< container::XIndexContainer>& xIndexContainer, + const OUString& sModuleIdentifier, + const OUString& sResourceURL) +{ + MigrationHashMap::iterator pFound = m_aOldVersionItemsHashMap.find(sResourceURL); + if (pFound==m_aOldVersionItemsHashMap.end()) + return; + + for (auto const& elem : pFound->second) + { + uno::Reference< container::XIndexContainer > xTemp = xIndexContainer; + + OUString sParentNodeName = elem.m_sParentNodeName; + sal_Int32 nIndex = 0; + do { + std::u16string_view sToken( o3tl::trim(o3tl::getToken(sParentNodeName, 0, '|', nIndex)) ); + if (sToken.empty()) + break; + + sal_Int32 nCount = xTemp->getCount(); + for (sal_Int32 i=0; i<nCount; ++i) { + OUString sCommandURL; + OUString sLabel; + uno::Reference< container::XIndexContainer > xChild; + + uno::Sequence< beans::PropertyValue > aPropSeq; + xTemp->getByIndex(i) >>= aPropSeq; + for (beans::PropertyValue const & prop : std::as_const(aPropSeq)) { + OUString sPropName = prop.Name; + if ( sPropName == ITEM_DESCRIPTOR_COMMANDURL ) + prop.Value >>= sCommandURL; + else if ( sPropName == ITEM_DESCRIPTOR_LABEL ) + prop.Value >>= sLabel; + else if ( sPropName == ITEM_DESCRIPTOR_CONTAINER ) + prop.Value >>= xChild; + } + + if (sCommandURL == sToken) { + xTemp = xChild; + break; + } + } + + } while (nIndex >= 0); + + if (nIndex == -1) { + auto aProperties = vcl::CommandInfoProvider::GetCommandProperties(elem.m_sCommandURL, sModuleIdentifier); + uno::Sequence< beans::PropertyValue > aPropSeq { + beans::PropertyValue(ITEM_DESCRIPTOR_COMMANDURL, 0, uno::Any(elem.m_sCommandURL), beans::PropertyState_DIRECT_VALUE), + beans::PropertyValue(ITEM_DESCRIPTOR_LABEL, 0, uno::Any(vcl::CommandInfoProvider::GetLabelForCommand(aProperties)), beans::PropertyState_DIRECT_VALUE), + beans::PropertyValue(ITEM_DESCRIPTOR_CONTAINER, 0, uno::Any(elem.m_xPopupMenu), beans::PropertyState_DIRECT_VALUE) + }; + + if (elem.m_sPrevSibling.isEmpty()) + xTemp->insertByIndex(0, uno::Any(aPropSeq)); + else { + sal_Int32 nCount = xTemp->getCount(); + sal_Int32 i = 0; + for (; i<nCount; ++i) { + OUString sCmd; + uno::Sequence< beans::PropertyValue > aTempPropSeq; + xTemp->getByIndex(i) >>= aTempPropSeq; + for (beans::PropertyValue const & prop : std::as_const(aTempPropSeq)) { + if ( prop.Name == ITEM_DESCRIPTOR_COMMANDURL ) { + prop.Value >>= sCmd; + break; + } + } + + if (sCmd == elem.m_sPrevSibling) + break; + } + + xTemp->insertByIndex(i+1, uno::Any(aPropSeq)); + } + } + } + + if (xIndexContainer.is()) + xCfgManager->replaceSettings(sResourceURL, xIndexContainer); + + uno::Reference< ui::XUIConfigurationPersistence > xUIConfigurationPersistence(xCfgManager, uno::UNO_QUERY); + if (xUIConfigurationPersistence.is()) + xUIConfigurationPersistence->store(); +} + +uno::Reference< ui::XUIConfigurationManager > NewVersionUIInfo::getConfigManager(std::u16string_view sModuleShortName) const +{ + uno::Reference< ui::XUIConfigurationManager > xCfgManager; + + for ( const css::beans::PropertyValue& rProp : m_lCfgManagerSeq) { + if (rProp.Name == sModuleShortName) { + rProp.Value >>= xCfgManager; + break; + } + } + + return xCfgManager; +} + +uno::Reference< container::XIndexContainer > NewVersionUIInfo::getNewMenubarSettings(std::u16string_view sModuleShortName) const +{ + uno::Reference< container::XIndexContainer > xNewMenuSettings; + + for (auto const & prop : m_lNewVersionMenubarSettingsSeq) { + if (prop.Name == sModuleShortName) { + prop.Value >>= xNewMenuSettings; + break; + } + } + + return xNewMenuSettings; +} + +uno::Reference< container::XIndexContainer > NewVersionUIInfo::getNewToolbarSettings(std::u16string_view sModuleShortName, std::u16string_view sToolbarName) const +{ + uno::Reference< container::XIndexContainer > xNewToolbarSettings; + + for (auto const & newProp : m_lNewVersionToolbarSettingsSeq) { + if (newProp.Name == sModuleShortName) { + uno::Sequence< beans::PropertyValue > lToolbarSettingsSeq; + newProp.Value >>= lToolbarSettingsSeq; + for (auto const & prop : std::as_const(lToolbarSettingsSeq)) { + if (prop.Name == sToolbarName) { + prop.Value >>= xNewToolbarSettings; + break; + } + } + + break; + } + } + + return xNewToolbarSettings; +} + +void NewVersionUIInfo::init(const std::vector< MigrationModuleInfo >& vModulesInfo) +{ + m_lCfgManagerSeq.resize(vModulesInfo.size()); + m_lNewVersionMenubarSettingsSeq.realloc(vModulesInfo.size()); + auto p_lNewVersionMenubarSettingsSeq = m_lNewVersionMenubarSettingsSeq.getArray(); + m_lNewVersionToolbarSettingsSeq.realloc(vModulesInfo.size()); + auto p_lNewVersionToolbarSettingsSeq = m_lNewVersionToolbarSettingsSeq.getArray(); + + static constexpr OUStringLiteral sMenubarResourceURL(u"private:resource/menubar/menubar"); + static constexpr OUStringLiteral sToolbarResourcePre(u"private:resource/toolbar/"); + + uno::Reference< ui::XModuleUIConfigurationManagerSupplier > xModuleCfgSupplier = ui::theModuleUIConfigurationManagerSupplier::get( ::comphelper::getProcessComponentContext() ); + + for (size_t i=0; i<vModulesInfo.size(); ++i) { + OUString sModuleIdentifier = mapModuleShortNameToIdentifier(vModulesInfo[i].sModuleShortName); + if (!sModuleIdentifier.isEmpty()) { + uno::Reference< ui::XUIConfigurationManager > xCfgManager = xModuleCfgSupplier->getUIConfigurationManager(sModuleIdentifier); + m_lCfgManagerSeq[i].Name = vModulesInfo[i].sModuleShortName; + m_lCfgManagerSeq[i].Value <<= xCfgManager; + + if (vModulesInfo[i].bHasMenubar) { + p_lNewVersionMenubarSettingsSeq[i].Name = vModulesInfo[i].sModuleShortName; + p_lNewVersionMenubarSettingsSeq[i].Value <<= xCfgManager->getSettings(sMenubarResourceURL, true); + } + + sal_Int32 nToolbars = vModulesInfo[i].m_vToolbars.size(); + if (nToolbars > 0) { + uno::Sequence< beans::PropertyValue > lPropSeq(nToolbars); + auto plPropSeq = lPropSeq.getArray(); + for (sal_Int32 j=0; j<nToolbars; ++j) { + OUString sToolbarName = vModulesInfo[i].m_vToolbars[j]; + OUString sToolbarResourceURL = sToolbarResourcePre + sToolbarName; + + plPropSeq[j].Name = sToolbarName; + plPropSeq[j].Value <<= xCfgManager->getSettings(sToolbarResourceURL, true); + } + + p_lNewVersionToolbarSettingsSeq[i].Name = vModulesInfo[i].sModuleShortName; + p_lNewVersionToolbarSettingsSeq[i].Value <<= lPropSeq; + } + } + } +} + +} // namespace desktop + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |