/* * Copyright (C) 2008-2009 Patrick Ohly * Copyright (C) 2009 Intel Corporation * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include SE_BEGIN_CXX const char *const SourceAdminDataName = "adminData"; int ConfigVersions[CONFIG_LEVEL_MAX][CONFIG_VERSION_MAX] = { { CONFIG_ROOT_MIN_VERSION, CONFIG_ROOT_CUR_VERSION }, { CONFIG_CONTEXT_MIN_VERSION, CONFIG_CONTEXT_CUR_VERSION }, { CONFIG_PEER_MIN_VERSION, CONFIG_PEER_CUR_VERSION }, }; std::string ConfigLevel2String(ConfigLevel level) { switch (level) { case CONFIG_LEVEL_ROOT: return "config root"; break; case CONFIG_LEVEL_CONTEXT: return "context config"; break; case CONFIG_LEVEL_PEER: return "peer config"; break; default: return StringPrintf("config level %d (?)", level); break; } } PropertySpecifier PropertySpecifier::StringToPropSpec(const std::string &spec, int flags) { PropertySpecifier res; size_t slash = spec.find('/'); if (slash != spec.npos) { // no normalization needed at the moment res.m_source = spec.substr(0, slash); slash++; } else { slash = 0; } size_t at = spec.find('@', slash); if (at != spec.npos) { // Context or config? if (spec.find('@', at + 1) != spec.npos) { // has a second @ sign, must be config name res.m_config = spec.substr(at + 1); } else { // context, include leading @ sign res.m_config = spec.substr(at); } if (flags & NORMALIZE_CONFIG) { res.m_config = SyncConfig::normalizeConfigString(res.m_config, SyncConfig::NORMALIZE_LONG_FORMAT); } } else { at = spec.size(); } res.m_property = spec.substr(slash, at - slash); return res; } UserIdentity::UserIdentity() : m_provider(InitStateString(USER_IDENTITY_PLAIN_TEXT, false)) { } UserIdentity UserIdentity::fromString(const InitStateString &idString) { UserIdentity id; if (idString.wasSet()) { size_t off = idString.find(':'); if (off != idString.npos) { id.m_provider = off ? idString.substr(0, off) : InitStateString(USER_IDENTITY_PLAIN_TEXT, false); id.m_identity = idString.substr(off + 1); } else { id.m_provider = InitStateString(USER_IDENTITY_PLAIN_TEXT, false); id.m_identity = idString; } } return id; } InitStateString UserIdentity::toString() const { std::string str; if (m_provider.wasSet()) { str += m_provider; str += ':'; } str += m_identity; return InitStateString(str, m_provider.wasSet() || m_identity.wasSet()); } std::string PropertySpecifier::toString() { std::string res; res.reserve(m_source.size() + 1 + m_property.size() + 1 + m_config.size()); res += m_source; if (!m_source.empty()) { res += '/'; } res += m_property; if (!m_config.empty()) { if (m_config[0] != '@') { res += '@'; } res += m_config; } return res; } string ConfigProperty::getName(const ConfigNode &node) const { if (m_names.empty()) { // shouldn't happen return "???"; } if (m_names.size() == 1) { // typical case for most properties return m_names.front(); } // pick the name already used in the node BOOST_FOREACH(const std::string &name, m_names) { string value; if (node.getProperty(name, value)) { return name; } } // main name as fallback return m_names.front(); } void ConfigProperty::splitComment(const string &comment, list &commentLines) { size_t start = 0; while (true) { size_t end = comment.find('\n', start); if (end == comment.npos) { commentLines.push_back(comment.substr(start)); break; } else { commentLines.push_back(comment.substr(start, end - start)); start = end + 1; } } } void ConfigProperty::throwValueError(const ConfigNode &node, const string &name, const string &value, const string &error) const { Exception::throwError(SE_HERE, node.getName() + ": " + name + " = " + value + ": " + error); } std::string ConfigProperty::sharing2str(Sharing sharing) { switch (sharing) { case GLOBAL_SHARING: return "global"; break; case SOURCE_SET_SHARING: return "shared"; break; case NO_SHARING: return "unshared"; break; } return "???"; } string SyncConfig::normalizeConfigString(const string &config, NormalizeFlags flags) { string normal = config; boost::to_lower(normal); BOOST_FOREACH(char &character, normal) { if (!isprint(character) || character == '/' || character == '\\' || character == ':') { character = '_'; } } if (boost::ends_with(normal, "@default")) { if (flags & NORMALIZE_SHORTHAND) { normal.resize(normal.size() - strlen("@default")); } } else if (boost::ends_with(normal, "@")) { normal.resize(normal.size() - 1); } else { size_t at = normal.rfind('@'); if (at == normal.npos && !(flags & NORMALIZE_IS_NEW)) { // No explicit context. Pick the first server which matches // when ignoring their context. Peer list is sorted by name, // therefore shorter config names (= without context) are // found first, as intended. BOOST_FOREACH(const StringPair &entry, getConfigs()) { string entry_peer, entry_context; splitConfigString(entry.first, entry_peer, entry_context); if (normal == entry_peer) { // found a matching, existing config, use it normal = entry.first; break; } } } if (!(flags & NORMALIZE_SHORTHAND) && normal.find('@') == normal.npos) { // explicitly include @default context specifier normal += "@default"; } } if (normal.empty()) { // default context is meant with the empty string, // better make that explicit normal = "@default"; } return normal; } std::string SyncConfig::DeviceDescription::getFingerprint() const { std::string fingerprint; /** In the case that we have the PnpInformation we prefer it over * the mutable device name. The is true even if we only found the * vendor component of the PnpInformation. */ if (m_pnpInformation) { if(m_pnpInformation->isKnownProduct()) fingerprint = m_pnpInformation->m_product; else fingerprint = m_pnpInformation->m_vendor; } else { fingerprint = m_deviceName; } return fingerprint; } bool SyncConfig::splitConfigString(const string &config, string &peer, string &context) { string::size_type at = config.rfind('@'); if (at != config.npos) { peer = config.substr(0, at); context = config.substr(at + 1); return true; } else { peer = config; context = "default"; return false; } } static SyncConfig::ConfigWriteMode defaultConfigWriteMode() { return SyncContext::isStableRelease() ? SyncConfig::MIGRATE_AUTOMATICALLY : SyncConfig::ASK_USER_TO_MIGRATE; } SyncConfig::SyncConfig() : m_layout(HTTP_SERVER_LAYOUT), // use more compact layout with shorter paths and less source nodes m_configWriteMode(defaultConfigWriteMode()) { // initialize properties SyncConfig::getRegistry(); SyncSourceConfig::getRegistry(); m_peerPath = m_contextPath = "volatile"; makeVolatile(); } void SyncConfig::makeVolatile() { m_tree.reset(new VolatileConfigTree()); m_fileTree.reset(); m_peerNode.reset(new VolatileConfigNode()); m_hiddenPeerNode = m_peerNode; m_globalNode = m_peerNode; m_contextNode = m_peerNode; m_contextHiddenNode = m_peerNode; m_props[false] = m_peerNode; m_props[true] = m_peerNode; } void SyncConfig::makeEphemeral() { m_ephemeral = true; // m_hiddenPeerNode.reset(new VolatileConfigNode()); // m_contextHiddenNode = m_hiddenPeerNode; } /** * The goal is to have only one FileConfigTree instance per file system * location. This ensures that in-memory representations remain in sync * when instantiating a SyncConfig is created multiple times. In addition, * FilterConfigNodes also must only exit once per underlying node. This * allows sharing cached passwords between configs when using indirect * password lookup. * * The implementation in both cases is the same: keep a cache with * weak pointers. When asked for an instance, consult the cache first. * If the instance still exists, we can return that shared pointer. If * not, we create a new one. * * It is necessary to garbage-collect obsolete entries, because the * lookup parameters might never be used again. */ class ConfigCache { typedef std::map< std::pair, boost::weak_ptr > TreeMap; TreeMap m_trees; typedef std::map< ConfigNode *, boost::weak_ptr > NodeMap; NodeMap m_nodes; template void purge(M &map) { typename M::iterator it = map.begin(); while (it != map.end()) { if (!it->second.lock()) { typename M::iterator next = it; ++next; map.erase(it); it = next; } else { ++it; } } } void purge(); public: boost::shared_ptr createTree(const std::string &root, SyncConfig::Layout layout); /** * The filter is only installed when creating a new node. It is * assumed to be the same when reusing the node. */ boost::shared_ptr createNode(const boost::shared_ptr &node, const FilterConfigNode::ConfigFilter &filter = FilterConfigNode::ConfigFilter()); static ConfigCache &singleton(); }; ConfigCache &ConfigCache::singleton() { static ConfigCache instance; return instance; } void ConfigCache::purge() { purge(m_trees); purge(m_nodes); } boost::shared_ptr ConfigCache::createTree(const std::string &root, SyncConfig::Layout layout) { TreeMap::mapped_type &entry = m_trees[TreeMap::key_type(root, layout)]; boost::shared_ptr result; result = entry.lock(); if (!result) { result.reset(new FileConfigTree(root, layout)); entry = result; } purge(); return result; } boost::shared_ptr ConfigCache::createNode(const boost::shared_ptr &node, const FilterConfigNode::ConfigFilter &filter) { NodeMap::mapped_type &entry = m_nodes[node.get()]; boost::shared_ptr result; result = entry.lock(); if (!result) { result.reset(new FilterConfigNode(node, filter)); entry = result; } purge(); return result; } SyncConfig::SyncConfig(const string &peer, boost::shared_ptr tree, const string &redirectPeerRootPath) : m_layout(SHARED_LAYOUT), m_redirectPeerRootPath(redirectPeerRootPath), m_configWriteMode(defaultConfigWriteMode()) { // initialize properties SyncConfig::getRegistry(); SyncSourceConfig::getRegistry(); string root; m_peer = normalizeConfigString(peer); // except for SHARED_LAYOUT (set below), // everything is below the directory called like // the peer m_peerPath = m_contextPath = m_peer; if (tree.get() != NULL) { // existing tree points into simple configuration m_tree = tree; m_layout = HTTP_SERVER_LAYOUT; m_peerPath = m_contextPath = ""; } else { // search for configuration in various places... root = getOldRoot(); string path = root + "/" + m_peerPath; if (!access((path + "/spds/syncml/config.txt").c_str(), F_OK)) { m_layout = SYNC4J_LAYOUT; } else { m_layout = SHARED_LAYOUT; root = getNewRoot(); path = root + "/" + m_peerPath; if (!access((path + "/config.ini").c_str(), F_OK) && !access((path + "/sources").c_str(), F_OK) && access((path + "/peers").c_str(), F_OK)) { m_layout = HTTP_SERVER_LAYOUT; } else { // check whether config name specifies a context, // otherwise use "default" splitConfigString(m_peer, m_peerPath, m_contextPath); if (!m_peerPath.empty()) { m_peerPath = m_contextPath + "/peers/" + m_peerPath; } } } m_fileTree = ConfigCache::singleton().createTree(root, m_layout); m_tree = m_fileTree; } string path; boost::shared_ptr node; switch (m_layout) { case SYNC4J_LAYOUT: // all properties reside in the same node path = m_peerPath + "/spds/syncml"; node = m_tree->open(path, ConfigTree::visible); m_peerNode = ConfigCache::singleton().createNode(node); m_globalNode = m_contextNode = m_peerNode; m_hiddenPeerNode = m_contextHiddenNode = m_globalHiddenNode = node; m_props[false] = m_peerNode; m_props[true] = ConfigCache::singleton().createNode(m_hiddenPeerNode); break; case HTTP_SERVER_LAYOUT: { // properties which are normally considered shared are // stored in the same nodes as the per-peer properties, // except for global ones path = ""; node = m_tree->open(path, ConfigTree::visible); m_globalNode = ConfigCache::singleton().createNode(node); node = m_tree->open(path, ConfigTree::hidden); m_globalHiddenNode = node; path = m_peerPath; node = m_tree->open(path, ConfigTree::visible); m_peerNode = ConfigCache::singleton().createNode(node); m_contextNode = m_peerNode; m_hiddenPeerNode = m_contextHiddenNode = m_tree->open(path, ConfigTree::hidden); // similar multiplexing as for SHARED_LAYOUT, // with two nodes underneath boost::shared_ptr mnode; mnode.reset(new MultiplexConfigNode(m_peerNode->getName(), getRegistry(), false)); m_props[false] = mnode; mnode->setNode(false, ConfigProperty::GLOBAL_SHARING, m_globalNode); mnode->setNode(false, ConfigProperty::SOURCE_SET_SHARING, m_peerNode); mnode->setNode(false, ConfigProperty::NO_SHARING, m_peerNode); mnode.reset(new MultiplexConfigNode(m_peerNode->getName(), getRegistry(), true)); m_props[true] = mnode; mnode->setNode(true, ConfigProperty::GLOBAL_SHARING, m_globalHiddenNode); mnode->setNode(true, ConfigProperty::SOURCE_SET_SHARING, m_peerNode); mnode->setNode(true, ConfigProperty::NO_SHARING, m_peerNode); break; } case SHARED_LAYOUT: // really use different nodes for everything path = ""; node = m_tree->open(path, ConfigTree::visible); m_globalNode = ConfigCache::singleton().createNode(node); node = m_tree->open(path, ConfigTree::hidden); m_globalHiddenNode = node; path = m_peerPath; if (path.empty()) { if (!m_redirectPeerRootPath.empty()) { node.reset(new IniFileConfigNode(m_redirectPeerRootPath, ".internal.ini", false)); node = m_tree->add(m_redirectPeerRootPath + "/.internal.ini", node); } else { node.reset(new DevNullConfigNode(m_contextPath + " without peer config")); } } else { node = m_tree->open(path, ConfigTree::visible); } m_peerNode = ConfigCache::singleton().createNode(node); if (path.empty()) { m_hiddenPeerNode = m_peerNode; } else { m_hiddenPeerNode = m_tree->open(path, ConfigTree::hidden); } path = m_contextPath; node = m_tree->open(path, ConfigTree::visible); m_contextNode = ConfigCache::singleton().createNode(node); m_contextHiddenNode = m_tree->open(path, ConfigTree::hidden); // Instantiate multiplexer with the most specific node name in // the set, the peer node's name. This is slightly inaccurate: // error messages generated for this node in will reference // the wrong config.ini file for shared properties. But // there no shared properties which can trigger such an error // at the moment, so this is good enough for now (MB#8037). boost::shared_ptr mnode; mnode.reset(new MultiplexConfigNode(m_peerNode->getName(), getRegistry(), false)); mnode->setHavePeerNodes(!m_peerPath.empty()); m_props[false] = mnode; mnode->setNode(false, ConfigProperty::GLOBAL_SHARING, m_globalNode); mnode->setNode(false, ConfigProperty::SOURCE_SET_SHARING, m_contextNode); mnode->setNode(false, ConfigProperty::NO_SHARING, m_peerNode); mnode.reset(new MultiplexConfigNode(m_hiddenPeerNode->getName(), getRegistry(), true)); mnode->setHavePeerNodes(!m_peerPath.empty()); m_props[true] = mnode; mnode->setNode(true, ConfigProperty::SOURCE_SET_SHARING, m_contextHiddenNode); mnode->setNode(true, ConfigProperty::NO_SHARING, m_hiddenPeerNode); mnode->setNode(true, ConfigProperty::GLOBAL_SHARING, m_globalHiddenNode); break; } // read version check for (ConfigLevel level = CONFIG_LEVEL_ROOT; level < CONFIG_LEVEL_MAX; level = (ConfigLevel)(level + 1)) { if (exists(level)) { if (getConfigVersion(level, CONFIG_MIN_VERSION) > ConfigVersions[level][CONFIG_CUR_VERSION]) { SE_LOG_INFO(NULL, "config version check failed: %s has format %d, but this SyncEvolution release only supports format %d", ConfigLevel2String(level).c_str(), getConfigVersion(level, CONFIG_MIN_VERSION), ConfigVersions[level][CONFIG_CUR_VERSION]); // our code is too old to read the config, reject it SE_THROW_EXCEPTION_STATUS(StatusException, StringPrintf("SyncEvolution %s is too old to read configuration '%s', please upgrade SyncEvolution.", VERSION, peer.c_str()), STATUS_RELEASE_TOO_OLD); } } } // Note that the version check does not reject old configs because // they are too old; so far, any release must be able to read any // older config. } void SyncConfig::prepareConfigForWrite() { // check versions before bumping to something incompatible with the // previous user of the config for (ConfigLevel level = CONFIG_LEVEL_ROOT; level < CONFIG_LEVEL_MAX; level = (ConfigLevel)(level + 1)) { if (getLayout() < SHARED_LAYOUT && level < CONFIG_LEVEL_PEER) { // old configs do not have explicit root or context, // only check peer config itself continue; } if (exists(level)) { if (getConfigVersion(level, CONFIG_CUR_VERSION) < ConfigVersions[level][CONFIG_MIN_VERSION]) { // release which created config will no longer be able to read // updated config; either alert user or migrate automatically string config; switch (level) { case CONFIG_LEVEL_CONTEXT: config = getContextName(); break; case CONFIG_LEVEL_PEER: config = getConfigName(); break; case CONFIG_LEVEL_ROOT: case CONFIG_LEVEL_MAX: // keep compiler happy, not reached for _MAX break; } SE_LOG_INFO(NULL, "must change format of %s '%s' in backward-incompatible way", ConfigLevel2String(level).c_str(), config.c_str()); if (m_configWriteMode == MIGRATE_AUTOMATICALLY) { // migrate config and anything beneath it, // so no further checking needed migrate(config); break; } else { SE_THROW_EXCEPTION_STATUS(StatusException, StringPrintf("Proceeding would modify config '%s' such " "that the previous SyncEvolution release " "will not be able to use it. Stopping now. " "Please explicitly acknowledge this step by " "running the following command on the command " "line: syncevolution --migrate '%s'", config.c_str(), config.c_str()), STATUS_MIGRATION_NEEDED); } } } } // now set current versions at all levels, // but without reducing versions: if a config has format // "cur = 10", then properties or features added in that // format remain even if the config is (temporarily?) used // by a SyncEvolution binary which has "cur = 5". for (ConfigLevel level = CONFIG_LEVEL_ROOT; level < CONFIG_LEVEL_MAX; level = (ConfigLevel)(level + 1)) { if (level == CONFIG_LEVEL_PEER && m_peerPath.empty()) { // no need (and no possibility) to set per-peer version) break; } for (ConfigLimit limit = CONFIG_MIN_VERSION; limit < CONFIG_VERSION_MAX; limit = (ConfigLimit)(limit + 1)) { // set if equal to ensure that version == 0 (the default) // is set explicitly if (getConfigVersion(level, limit) <= ConfigVersions[level][limit]) { setConfigVersion(level, limit, ConfigVersions[level][limit]); } } } flush(); } void SyncConfig::migrate(const std::string &config) { if (config.empty()) { // migrating root not yet supported SE_THROW("internal error, migrating config root not implemented"); } else { // migrate using the higher-level logic in the Cmdline class Cmdline migrate(m_peer.c_str(), "--migrate", config.c_str(), NULL); bool res = migrate.parse() && migrate.run(); if (!res) { SE_THROW(StringPrintf("migration of config '%s' failed", config.c_str())); } // files that our tree access may have changed, refresh our // in-memory copy m_tree->reload(); } } string SyncConfig::getRootPath() const { return m_fileTree ? normalizePath(m_fileTree->getRoot() + "/" + ((m_layout == SYNC4J_LAYOUT || hasPeerProperties()) ? m_peerPath : m_contextPath)) : ""; } void SyncConfig::addPeers(const string &root, const std::string &configname, SyncConfig::ConfigList &res) { FileConfigTree tree(root, SyncConfig::HTTP_SERVER_LAYOUT); list servers = tree.getChildren(""); BOOST_FOREACH(const string &server, servers) { // sanity check: only list server directories which actually // contain a configuration. To distinguish between a context // (~/.config/syncevolution/default) and an HTTP server config // (~/.config/syncevolution/scheduleworld), we check for the // "peer" subdirectory that is only in the former. // // Contexts which don't have a peer are therefore incorrectly // listed as a peer. Short of adding a special hidden file // this can't be fixed. This is probably overkill and thus not // done yet. string peerPath = server + "/peers"; if (!access((root + "/" + peerPath).c_str(), F_OK)) { // not a real HTTP server, search for peers BOOST_FOREACH(const string &peer, tree.getChildren(peerPath)) { res.push_back(pair (normalizeConfigString(peer + "@" + server), root + "/" + peerPath + "/" + peer)); } } else if (!access((root + "/" + server + "/" + configname).c_str(), F_OK)) { res.push_back(pair (server, root + "/" + server)); } } } /** returns true if a precedes b (strict weak ordering) */ static bool cmpConfigEntries(const StringPair &a, const StringPair &b) { string peerA, contextA, peerB, contextB; SyncConfig::splitConfigString(a.first, peerA, contextA); SyncConfig::splitConfigString(b.first, peerB, contextB); int res; res = contextA.compare(contextB); if (res == 0) { res = peerA.compare(peerB); if (res == 0) { res = a.second.compare(b.second); } } return res < 0; } SyncConfig::ConfigList SyncConfig::getConfigs() { ConfigList res; addPeers(getOldRoot(), "config.txt", res); addPeers(getNewRoot(), "config.ini", res); // Sort the list by (context, peer name, path); // better than returning it in random order. // This sort order (compared to simple lexical // sorting based on the full config name) has // the advantage that peer names or contexts with // suffix (foo.old vs. foo) come later. res.sort(cmpConfigEntries); return res; } static string SyncEvolutionTemplateDir() { string templateDir(TEMPLATE_DIR); const char *envvar = getenv("SYNCEVOLUTION_TEMPLATE_DIR"); if (envvar) { templateDir = envvar; } return templateDir; } SyncConfig::TemplateList SyncConfig::matchPeerTemplates(const DeviceList &peers, bool fuzzyMatch) { TemplateList result; // match against all possible templates without any assumption on directory // layout, the match is entirely based on the metadata template.ini string templateDir(SyncEvolutionTemplateDir()); std::queue > directories; directories.push(templateDir); templateDir = SubstEnvironment("${XDG_CONFIG_HOME}/syncevolution-templates"); directories.push(templateDir); while (!directories.empty()) { string sDir = directories.front(); directories.pop(); if (isDir(sDir)) { // check all sub directories ReadDir dir(sDir); BOOST_FOREACH(const string &entry, dir) { // ignore hidden files, . and .. if (!boost::starts_with(entry, ".")) { directories.push(sDir + "/" + entry); } } } else { TemplateConfig templateConf (sDir); if (boost::ends_with(sDir, "~") || !templateConf.isTemplateConfig()) { // ignore temporary files and files which do // not contain a valid template continue; } BOOST_FOREACH (const DeviceList::value_type &entry, peers){ std::string fingerprint(entry.getFingerprint()); // peerName should be empty if no reliable device info is on hand. std::string peerName = entry.m_pnpInformation ? fingerprint : ""; int rank = templateConf.metaMatch (entry.getFingerprint(), entry.m_matchMode); if (fuzzyMatch){ if (rank > TemplateConfig::NO_MATCH) { result.push_back (boost::shared_ptr( new TemplateDescription(templateConf.getTemplateId(), templateConf.getDescription(), rank, peerName, entry.m_deviceId, entry.m_deviceName, sDir, templateConf.getFingerprint(), templateConf.getTemplateName() ) )); } } else if (rank == TemplateConfig::BEST_MATCH){ result.push_back (boost::shared_ptr( new TemplateDescription(templateConf.getTemplateId(), templateConf.getDescription(), rank, peerName, entry.m_deviceId, entry.m_deviceName, sDir, templateConf.getFingerprint(), templateConf.getTemplateName()) )); break; } } } } result.sort (TemplateDescription::compare_op); return result; } boost::shared_ptr SyncConfig::createPeerTemplate(const string &server) { if (server.empty()) { // Empty template name => no such template. This check is // necessary because otherwise we end up with SyncConfig(""), // which is a configuration where peer-specific properties // cannot be set, triggering an errror in config->setDevID(). return boost::shared_ptr(); } // case insensitive search for read-only file template config string templateConfig(SyncEvolutionTemplateDir()); // before starting another fuzzy match process, first try to load the // template directly taking the parameter as the path if (server == "none") { // nothing to read from, just set some defaults below } else if (TemplateConfig::isTemplateConfig(server)) { templateConfig = server; } else { SyncConfig::DeviceList devices; devices.push_back (DeviceDescription("", server, MATCH_ALL)); templateConfig = ""; TemplateList templates = matchPeerTemplates (devices, false); if (!templates.empty()) { templateConfig = templates.front()->m_path; } if (templateConfig.empty()) { // return "not found" return boost::shared_ptr(); } } boost::shared_ptr tree(new SingleFileConfigTree(templateConfig)); boost::shared_ptr config(new SyncConfig(server, tree)); boost::shared_ptr source; config->setDefaults(false); config->setDevID(string("syncevolution-") + UUID()); // leave the rest empty for special "none" template if (server == "none") { return config; } // check for icon if (config->getIconURI().empty()) { string dirname, filename; splitPath(templateConfig, dirname, filename); ReadDir dir(getDirname(dirname)); // remove last suffix, regardless what it is size_t pos = filename.rfind('.'); if (pos != filename.npos) { filename.resize(pos); } filename += "-icon"; BOOST_FOREACH(const string &entry, dir) { if (boost::istarts_with(entry, filename)) { config->setIconURI("file://" + dirname + "/" + entry); break; } } } // "default" maps to SyncEvolution server template, which is not // consumer ready. When used as "default" by the GTK sync UI, // the UI expects the "consumer ready" flag to be set. Do that // here. Also unset the peer name, because otherwise it shows // up in the UI. if (server == "default") { config->setConsumerReady(true); config->setUserPeerName(InitStateString()); } return config; } bool SyncConfig::exists() const { return m_peerPath.empty() ? m_contextNode->exists() : m_peerNode->exists(); } bool SyncConfig::exists(ConfigLevel level) const { switch (level) { case CONFIG_LEVEL_ROOT: return m_globalNode->exists(); break; case CONFIG_LEVEL_CONTEXT: return m_contextNode->exists(); break; case CONFIG_LEVEL_PEER: return m_peerNode->exists(); break; default: return false; } } string SyncConfig::getContextName() const { string peer, context; splitConfigString(getConfigName(), peer, context); return string("@") + context; } string SyncConfig::getPeerName() const { string peer, context; splitConfigString(getConfigName(), peer, context); return peer; } list SyncConfig::getPeers() const { list res; if (!hasPeerProperties()) { std::string rootPath = getRootPath(); if (!rootPath.empty()) { FileConfigTree tree(getRootPath(), SHARED_LAYOUT); res = tree.getChildren("peers"); } } return res; } void SyncConfig::preFlush(UserInterface &ui) { /* Iterator over all sync global and source properties * one by one and check whether they need to save password */ /* save password in the global config node */ ConfigPropertyRegistry& registry = getRegistry(); BOOST_FOREACH(const ConfigProperty *prop, registry) { prop->savePassword(ui, *this); } /** grep each source and save their password */ list configuredSources = getSyncSources(); BOOST_FOREACH(const string &sourceName, configuredSources) { //boost::shared_ptr sc = getSyncSourceConfig(sourceName); ConfigPropertyRegistry& registry = SyncSourceConfig::getRegistry(); SyncSourceNodes sourceNodes = getSyncSourceNodes(sourceName); BOOST_FOREACH(const ConfigProperty *prop, registry) { prop->savePassword(ui, *this, sourceName); } } } void SyncConfig::flush() { if (!isEphemeral()) { if (m_fileTree && m_layout == SHARED_LAYOUT && !hasPeerProperties()) { // Ensure that "peers" directory exists for new-style // configs. It would not get created when flushing nodes // for pure context configs otherwise (it's empty), and we // need it to detect new-syle configs. mkdir_p(m_fileTree->getRoot() + "/" + m_contextPath + "/peers"); } m_tree->flush(); } } void SyncConfig::remove() { boost::shared_ptr tree = m_tree; // stop using the config nodes, they might get removed now makeVolatile(); tree->remove(m_peerPath.empty() ? m_contextPath : m_peerPath); } boost::shared_ptr SyncConfig::getSyncSourceConfig(const string &name) { SyncSourceNodes nodes = getSyncSourceNodes(name); return boost::shared_ptr(new PersistentSyncSourceConfig(name, nodes)); } list SyncConfig::getSyncSources() const { // Return *all* sources configured in this context, // not just those configured for the peer. This // is necessary so that sources created for some other peer // show up for the current one, to prevent overwriting // existing properties unintentionally. // Returned sources are an union of: // 1. contextpath/sources // 2. peers/[one-peer]/sources // 3. sources in source filter set sources; list sourceList; if (m_layout == SHARED_LAYOUT) { // get sources in context sourceList = m_tree->getChildren(m_contextPath + "/sources"); sources.insert(sourceList.begin(), sourceList.end()); // get sources from peer if it's not empty and merge into // full set of sources if (!m_peerPath.empty()) { sourceList = m_tree->getChildren(m_peerPath + "/sources"); sources.insert(sourceList.begin(), sourceList.end()); } } else { // get sources from peer sourceList = m_tree->getChildren(m_peerPath + (m_layout == SYNC4J_LAYOUT ? "/spds/sources" : "/sources")); sources.insert(sourceList.begin(), sourceList.end()); } // get sources from filter and union them into returned sources BOOST_FOREACH(const SourceProps::value_type &value, m_sourceFilters) { if (value.first.empty()) { // ignore filter for all sources continue; } sources.insert(value.first); } // Convert back to simple list. As a nice side-effect of // temporarily using a set, the final list is sorted. return list(sources.begin(), sources.end()); } SyncSourceNodes SyncConfig::getSyncSourceNodes(const string &name, const string &changeId) { if (m_nodeCache.find(name) != m_nodeCache.end()) { // reuse existing set of nodes return m_nodeCache[name]; } /** shared source properties */ boost::shared_ptr sharedNode; /** per-peer source properties */ boost::shared_ptr peerNode; /** per-peer internal properties and meta data */ boost::shared_ptr hiddenPeerNode, serverNode, trackingNode; string cacheDir; // store configs lower case even if the UI uses mixed case string lower = name; boost::to_lower(lower); boost::shared_ptr node; string sharedPath, peerPath; switch (m_layout) { case SYNC4J_LAYOUT: peerPath = m_peerPath + "/spds/sources/" + lower; break; case HTTP_SERVER_LAYOUT: peerPath = m_peerPath + "/sources/" + lower; break; case SHARED_LAYOUT: if (!m_peerPath.empty()) { peerPath = m_peerPath + "/sources/" + lower; } sharedPath = m_contextPath + string("/sources/") + lower; break; } // Compatibility mode for reading configs which have "type" instead // of "backend/databaseFormat/syncFormat/forceSyncFormat": determine // the new values based on the old property, then inject the new // values into the SyncSourceNodes by adding an intermediate layer // of FilterConfigNodes. The top FilterConfigNode layer is the one // which might get modified, the one underneath remains hidden and // thus preserves the new values even if the caller does a setFilter(). bool compatMode = getConfigVersion(CONFIG_LEVEL_CONTEXT, CONFIG_CUR_VERSION) < 1; SourceType sourceType; if (compatMode) { node = m_tree->open(peerPath.empty() ? sharedPath : peerPath, ConfigTree::visible); string type; if (node->getProperty("type", type)) { sourceType = SourceType(type); } else { // not set: avoid compatibility mode compatMode = false; } } if (peerPath.empty()) { node.reset(new DevNullConfigNode(m_contextPath + " without peer configuration")); peerNode = ConfigCache::singleton().createNode(node); hiddenPeerNode = trackingNode = serverNode = node; } else { // Here we assume that m_tree is a FileConfigTree. Otherwise getRootPath() // will not point into a normal file system. We fall back to not allowing // the usage of a cache dir by using /dev/null in that case. std::string rootPath = getRootPath(); if (rootPath.empty()) { cacheDir = "/dev/null"; } else { cacheDir = rootPath + "/" + peerPath + "/.cache"; } node = m_tree->open(peerPath, ConfigTree::visible); if (compatMode) { boost::shared_ptr compat(new FilterConfigNode(node)); compat->addFilter("syncFormat", InitStateString(sourceType.m_format, !sourceType.m_format.empty())); compat->addFilter("forceSyncFormat", sourceType.m_forceFormat ? InitStateString("1", true) : InitStateString("0", false)); if (sharedPath.empty()) { compat->addFilter("databaseFormat", InitStateString(sourceType.m_localFormat, !sourceType.m_localFormat.empty())); compat->addFilter("backend", InitStateString(sourceType.m_backend, !sourceType.m_backend.empty())); } node = compat; } peerNode = ConfigCache::singleton().createNode(node, m_sourceFilters.createSourceFilter(name)); hiddenPeerNode = m_tree->open(peerPath, ConfigTree::hidden); trackingNode = m_tree->open(peerPath, ConfigTree::other, changeId); serverNode = m_tree->open(peerPath, ConfigTree::server, changeId); } if (isEphemeral()) { // Throw away meta data. trackingNode.reset(new VolatileConfigNode); hiddenPeerNode.reset(new VolatileConfigNode); serverNode.reset(new VolatileConfigNode); } else if (!m_redirectPeerRootPath.empty()) { // Local sync: overwrite per-peer nodes with nodes inside the // parents tree. Otherwise different configs syncing locally // against the same context end up sharing .internal.ini and // .other.ini files inside that context. string path = m_redirectPeerRootPath + "/sources/" + lower; trackingNode.reset(new IniHashConfigNode(path, ".other.ini", false)); trackingNode = m_tree->add(path + "/.other.ini", trackingNode); if (peerPath.empty()) { hiddenPeerNode = peerNode; } else { hiddenPeerNode = boost::static_pointer_cast(m_tree->add(path + "/.internal.ini", peerNode)); } } if (sharedPath.empty()) { sharedNode = peerNode; } else { node = m_tree->open(sharedPath, ConfigTree::visible); if (compatMode) { boost::shared_ptr compat(new FilterConfigNode(node)); compat->addFilter("databaseFormat", InitStateString(sourceType.m_localFormat, !sourceType.m_localFormat.empty())); compat->addFilter("backend", InitStateString(sourceType.m_backend, !sourceType.m_backend.empty())); node = compat; } sharedNode = ConfigCache::singleton().createNode(node, m_sourceFilters.createSourceFilter(name)); } SyncSourceNodes nodes(!peerPath.empty(), sharedNode, peerNode, hiddenPeerNode, trackingNode, serverNode, cacheDir); m_nodeCache.insert(make_pair(name, nodes)); return nodes; } ConstSyncSourceNodes SyncConfig::getSyncSourceNodes(const string &name, const string &changeId) const { return const_cast(this)->getSyncSourceNodes(name, changeId); } SyncSourceNodes SyncConfig::getSyncSourceNodesNoTracking(const string &name) { SyncSourceNodes nodes = getSyncSourceNodes(name); boost::shared_ptr dummy(new VolatileConfigNode()); return SyncSourceNodes(nodes.m_havePeerNode, nodes.m_sharedNode, nodes.m_peerNode, nodes.m_hiddenPeerNode, dummy, nodes.m_serverNode, nodes.m_cacheDir); } static ConfigProperty syncPropSyncURL("syncURL", "Identifies how to contact the peer,\n" "best explained with some examples.\n\n" "HTTP(S) SyncML servers::\n\n" " http://example.com/sync\n\n" "OBEX over Bluetooth uses the MAC address, with\n" "the channel chosen automatically::\n\n" " obex-bt://00:0A:94:03:F3:7E\n\n" "If the automatism fails, the channel can also be specified::\n\n" " obex-bt://00:0A:94:03:F3:7E+16\n\n" "For peers contacting us via Bluetooth, the MAC address is\n" "used to identify it before the sync starts. Multiple\n" "urls can be specified in one syncURL property::\n\n" " obex-bt://00:0A:94:03:F3:7E obex-bt://00:01:02:03:04:05\n\n" "In the future this might be used to contact the peer\n" "via one of several transports; right now, only the first\n" "one is tried." // MB #9446 ); static ConfigProperty syncPropDevID("deviceId", "The SyncML server gets this string and will use it to keep track of\n" "changes that still need to be synchronized with this particular\n" "client; it must be set to something unique (like the pseudo-random\n" "string created automatically for new configurations) among all clients\n" "accessing the same server.\n" "myFUNAMBOL also requires that the string starts with sc-pim-"); static ConfigProperty syncPropUsername("username", "user name used for authorization with the SyncML server", ""); static BoolConfigProperty syncPropPeerIsClient("PeerIsClient", "Indicates whether this configuration is about a\n" "client peer or server peer.\n", "FALSE"); static SafeConfigProperty syncPropRemoteDevID("remoteDeviceId", "SyncML ID of our peer, empty if unknown; must be set only when\n" "the peer is a SyncML client contacting us via HTTP.\n" "Clients contacting us via OBEX/Bluetooth can be identified\n" "either via this remoteDeviceId property or by their MAC\n" "address, if that was set in the syncURL property.\n" "\n" "If this property is empty and the peer synchronizes with\n" "this configuration chosen by some other means, then its ID\n" "is recorded here automatically and later used to verify that\n" "the configuration is not accidentally used by a different\n" "peer."); static class SyncPasswordConfigProperty : public PasswordConfigProperty { public: SyncPasswordConfigProperty() : PasswordConfigProperty("password", "password used for authorization with the peer;\n" "in addition to specifying it directly as plain text, it can\n" "also be read from the standard input or from an environment\n" "variable of your choice::\n\n" " plain text : password = \n" " ask : password = -\n" " env variable: password = ${}\n") {} virtual void checkPassword(UserInterface &ui, SyncConfig &config, int flags, const std::string &sourceName = "") const { PasswordConfigProperty::checkPassword(ui, config, flags, syncPropUsername, sourceName); } virtual void savePassword(UserInterface &ui, SyncConfig &config, const std::string &sourceName = "") const { PasswordConfigProperty::savePassword(ui, config, syncPropUsername, sourceName); } ConfigPasswordKey getPasswordKey(const string &descr, const string &serverName, FilterConfigNode &globalConfigNode, const string &sourceName, const boost::shared_ptr &sourceConfigNode) const { ConfigPasswordKey key; bool peerIsClient = syncPropPeerIsClient.getPropertyValue(globalConfigNode); key.server = syncPropSyncURL.getProperty(globalConfigNode); key.description = StringPrintf("sync password for %s", descr.c_str()); if (peerIsClient && key.server.empty()) { /** * Fall back to username/remoteDeviceId as key. */ key.server = syncPropRemoteDevID.getProperty(globalConfigNode); } else { /** * Here we use server sync url without protocol prefix and * user account name as the key in the keyring. * The URL must not be empty, otherwise we end up * overwriting the password of some other service just * because it happens to have the same username. */ size_t start = key.server.find("://"); /* we don't preserve protocol prefix for it may change */ if (start != key.server.npos) { key.server = key.server.substr(start + 3); } } key.user = getUsername(syncPropUsername, globalConfigNode); // User domain part of a username which looks like an email address // as server name if we don't have something better. Needed for // WebDAV configs using just an email address to locate the server. if (key.server.empty()) { size_t offset = key.user.find('@'); if (offset != key.user.npos && offset != key.user.size() - 1) { key.server = key.user.substr(offset + 1); key.user.resize(offset); } } return key; } } syncPropPassword; static BoolConfigProperty syncPropPreventSlowSync("preventSlowSync", "During a slow sync, the SyncML server must match all items\n" "of the client with its own items and detect which ones it\n" "already has based on properties of the items. This is slow\n" "(client must send all its data) and can lead to duplicates\n" "(when the server fails to match correctly).\n" "It is therefore sometimes desirable to wipe out data on one\n" "side with a refresh-from-client/server sync instead of doing\n" "a slow sync.\n" "When this option is enabled, slow syncs that could cause problems\n" "are not allowed to proceed. Instead, the affected datastores are\n" "skipped, allowing the user to choose a suitable sync mode in\n" "the next run (slow sync selected explicitly, refresh sync).\n" "The following situations are handled:\n\n" "- running as client with no local data => unproblematic,\n" " slow sync is allowed to proceed automatically\n" "- running as client with local data => client has no\n" " information about server, so slow sync might be problematic\n" " and is prevented\n" "- client has data, server asks for slow sync because all its data\n" " was deleted (done by Memotoo and Mobical, because they treat\n" " this as 'user wants to start from scratch') => the sync would\n" " recreate all the client's data, even if the user really wanted\n" " to have it deleted, therefore slow sync is prevented\n", "TRUE"); static BoolConfigProperty syncPropUseProxy("useProxy", "set to T to choose an HTTP proxy explicitly; otherwise the default\n" "proxy settings of the underlying HTTP transport mechanism are used;\n" "only relevant when contacting the peer via HTTP", "FALSE"); static ConfigProperty syncPropProxyHost("proxyHost", "proxy URL (``http://:``)"); static ConfigProperty syncPropProxyUsername("proxyUsername", "authentication for proxy: username"); static class ProxyPasswordConfigProperty : public PasswordConfigProperty { public: ProxyPasswordConfigProperty() : PasswordConfigProperty("proxyPassword", "proxy password, can be specified in different ways,\n" "see SyncML server password for details\n", "", "proxy") {} /** * re-implement this function for it is necessary to do a check * before retrieving proxy password */ virtual void checkPassword(UserInterface &ui, SyncConfig &config, int flags, const std::string &sourceName = std::string()) const { /* if useProxy is set 'true', then check proxypassword */ if (config.getUseProxy()) { PasswordConfigProperty::checkPassword(ui, config, flags, syncPropProxyUsername, sourceName); } } virtual void savePassword(UserInterface &ui, SyncConfig &config, const std::string &sourceName = std::string()) const { PasswordConfigProperty::savePassword(ui, config, syncPropProxyUsername, sourceName); } virtual ConfigPasswordKey getPasswordKey(const std::string &descr, const std::string &serverName, FilterConfigNode &globalConfigNode, const std::string &sourceName, const boost::shared_ptr &sourceConfigNode) const { ConfigPasswordKey key; key.server = syncPropProxyHost.getProperty(globalConfigNode); key.user = getUsername(syncPropProxyUsername, globalConfigNode); key.description = StringPrintf("proxy password for %s", descr.c_str()); return key; } } syncPropProxyPassword; static StringConfigProperty syncPropClientAuthType("clientAuthType", "- empty or \"md5\" for secure method (recommended)\n" "- \"basic\" for insecure method\n" "\n" "This setting is only for debugging purpose and only\n" "has an effect during the initial sync of a client.\n" "Later it remembers the method that was supported by\n" "the server and uses that. When acting as server,\n" "clients contacting us can use both basic and md5\n" "authentication.\n", "md5", "", Values() + (Aliases("basic") + "syncml:auth-basic") + (Aliases("md5") + "syncml:auth-md5" + "")); static ULongConfigProperty syncPropMaxMsgSize("maxMsgSize", "The maximum size of each message can be set (maxMsgSize) and the\n" "peer can be told to never sent items larger than a certain\n" "threshold (maxObjSize). Presumably the peer has to truncate or\n" "skip larger items. Sizes are specified as number of bytes.", "150000"); static UIntConfigProperty syncPropMaxObjSize("maxObjSize", "", "4000000"); static BoolConfigProperty syncPropWBXML("enableWBXML", "use the more compact binary XML (WBXML) for messages between client and server;\n" "not applicable when the peer is a SyncML client, because then the client\n" "chooses the encoding", "TRUE"); static BoolConfigProperty syncPropRefreshSync("enableRefreshSync", "Use the more advanced refresh-from-server sync mode to\n" "implement the refresh-from-remote operation. Some SyncML\n" "servers do not support this. Therefore the default is to\n" "delete local data before doing a slow sync, which has the\n" "same effect. However, some servers work better when they\n" "are told explicitly that the sync is a refresh sync. For\n" "example, Funambol's One Media server rejects too many slow\n" "syncs in a row with a 417 'retry later' error.\n", "FALSE"); static ConfigProperty syncPropLogDir("logdir", "full path to directory where automatic backups and logs\n" "are stored for all synchronizations; if unset, then\n" "\"${XDG_CACHE_HOME}/syncevolution/\" (which\n" "usually expands to ${HOME}/.cache/...) will be used;\n" "if \"none\", then no backups of the databases are made and any\n" "output is printed directly to the screen"); static UIntConfigProperty syncPropMaxLogDirs("maxlogdirs", "Controls how many session directories are kept at most in the logdir.\n" "Unless set to zero, SyncEvolution will remove old directories and\n" "all their content to prevent the number of log directories from\n" "growing beyond the given limit. It tries to be intelligent and will\n" "remove sessions in which nothing interesting happened (no errors,\n" "no data changes) in favor of keeping sessions where something\n" "happened, even if those sessions are older.", "10"); static UIntConfigProperty syncPropLogLevel("loglevel", "level of detail for log messages:\n" "- 0 (or unset) = INFO messages without log file, DEBUG with log file\n" "- 1 = only ERROR messages\n" "- 2 = also INFO messages\n" "- 3 = also DEBUG messages\n" "> 3 = increasing amounts of debug messages for developers"); static UIntConfigProperty syncPropNotifyLevel("notifyLevel", "Level of detail for desktop notifications. Currently such\n" "notifications are generated only for automatically started\n" "sync sessions.\n" "\n" "0 - suppress all notifications\n" "1 - show only errors\n" "2 - show information about changes and errors (in practice currently the same as level 3)\n" "3 - show all notifications, including starting a sync\n", "3"); static BoolConfigProperty syncPropPrintChanges("printChanges", "enables or disables the detailed (and sometimes slow) comparison\n" "of database content before and after a sync session", "TRUE"); static BoolConfigProperty syncPropDumpData("dumpData", "enables or disables the automatic backup of database content\n" "before and after a sync session (always enabled if printChanges is enabled)", "TRUE"); static SecondsConfigProperty syncPropRetryDuration("RetryDuration", "The total amount of time in seconds in which the SyncML\n" "client tries to get a response from the server.\n" "During this time, the client will resend messages\n" "in regular intervals (RetryInterval) if no response\n" "is received or the message could not be delivered due\n" "to transport problems. When this time is exceeded\n" "without a response, the synchronization aborts without\n" "sending further messages to the server.\n" "\n" "When acting as server, this setting controls how long\n" "a client is allowed to not send a message before the\n" "synchronization is aborted." ,"5M"); static SecondsConfigProperty syncPropRetryInterval("RetryInterval", "The number of seconds between the start of SyncML message sending\n" "and the start of the retransmission. If the interval has\n" "already passed when a message send returns, the\n" "message is resent immediately. Resending without\n" "any delay will never succeed and therefore specifying 0\n" "disables retries.\n" "\n" "Servers cannot resend messages, so this setting has no\n" "effect in that case.\n" "\n" "The WebDAV backend also resends messages after a temporary\n" "network error. It uses exponential backoff to determine when\n" "the server is available again. This setting is divided by 24\n" "to obtain the initial delay (default: 2m => 5s), which is then\n" "doubled for each retry." ,"2M"); static SafeConfigProperty syncPropPeerName("PeerName", "An arbitrary name for the peer referenced by this config.\n" "Might be used by a GUI. The command line tool always uses the\n" "the configuration name."); static ConfigProperty syncPropSyncMLVersion("SyncMLVersion", "On a client, the latest commonly supported SyncML version\n" "is used when contacting a server. One of '1.0/1.1/1.2' can\n" "be used to pick a specific version explicitly.\n" "\n" "On a server, this option controls what kind of Server Alerted\n" "Notification is sent to the client to start a synchronization.\n" "By default, first the format from 1.2 is tried, then in case\n" "of failure, the older one from 1.1. 1.2/1.1 can be set\n" "explicitly, which disables the automatism.\n" "\n" "Instead or in adddition to the version, several keywords can\n" "be set in this property (separated by spaces or commas):\n" "\n" "- NOCTCAP - avoid sending CtCap meta information\n" "- NORESTART - disable the sync mode extension that SyncEvolution\n" " client and server use to negotiate whether both sides support\n" " running multiple sync iterations in the same session\n" "- REQUESTMAXTIME=