Imported Upstream version 1.2.99~20120606~SE~ff65aef~SYSYNC~2728cb4
[platform/upstream/syncevolution.git] / src / syncevo / Cmdline.cpp
1 /*
2  * Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
3  * Copyright (C) 2009 Intel Corporation
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) version 3.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18  * 02110-1301  USA
19  */
20
21 #include <syncevo/Cmdline.h>
22 #include <syncevo/CmdlineSyncClient.h>
23 #include <syncevo/FilterConfigNode.h>
24 #include <syncevo/VolatileConfigNode.h>
25 #include <syncevo/IniConfigNode.h>
26 #include <syncevo/SyncSource.h>
27 #include <syncevo/SyncContext.h>
28 #include <syncevo/util.h>
29 #include "test.h"
30
31 #include <synthesis/SDK_util.h>
32
33 #include <unistd.h>
34 #include <errno.h>
35
36 #include <fstream>
37 #include <iostream>
38 #include <iomanip>
39 #include <sstream>
40 #include <memory>
41 #include <set>
42 #include <list>
43 #include <algorithm>
44 using namespace std;
45
46 #include <boost/shared_ptr.hpp>
47 #include <boost/algorithm/string/join.hpp>
48 #include <boost/algorithm/string/split.hpp>
49 #include <boost/algorithm/string.hpp>
50 #include <boost/tokenizer.hpp>
51 #include <boost/foreach.hpp>
52 #include <boost/range.hpp>
53 #include <fstream>
54
55 #include <syncevo/declarations.h>
56 using namespace std;
57 SE_BEGIN_CXX
58
59 // synopsis and options char strings
60 #include "CmdlineHelp.c"
61
62 Cmdline::Cmdline(int argc, const char * const * argv) :
63     m_argc(argc),
64     m_argv(argv),
65     m_validSyncProps(SyncConfig::getRegistry()),
66     m_validSourceProps(SyncSourceConfig::getRegistry())
67 {}
68
69 Cmdline::Cmdline(const vector<string> &args) :
70     m_args(args),
71     m_validSyncProps(SyncConfig::getRegistry()),
72     m_validSourceProps(SyncSourceConfig::getRegistry())
73 {
74     m_argc = args.size();
75     m_argvArray.reset(new const char *[args.size()]);
76     for(int i = 0; i < m_argc; i++) {
77         m_argvArray[i] = m_args[i].c_str();
78     }
79     m_argv = m_argvArray.get();
80 }
81
82 Cmdline::Cmdline(const char *arg, ...) :
83     m_validSyncProps(SyncConfig::getRegistry()),
84     m_validSourceProps(SyncSourceConfig::getRegistry())
85 {
86     va_list argList;
87     va_start(argList, arg);
88     for (const char *curr = arg;
89          curr;
90          curr = va_arg(argList, const char *)) {
91         m_args.push_back(curr);
92     }
93     va_end(argList);
94     m_argc = m_args.size();
95     m_argvArray.reset(new const char *[m_args.size()]);
96     for (int i = 0; i < m_argc; i++) {
97         m_argvArray[i] = m_args[i].c_str();
98     }
99     m_argv = m_argvArray.get();
100 }
101
102 bool Cmdline::parse()
103 {
104     vector<string> parsed;
105     return parse(parsed);
106 }
107
108 /**
109  * Detects "--sync foo", "--sync=foo", "-s foo".
110  */
111 static bool IsKeyword(const std::string arg,
112                       const char *longWord,
113                       const char *shortWord)
114 {
115     return boost::istarts_with(arg, std::string(longWord) + "=") ||
116         boost::iequals(arg, longWord) ||
117         boost::iequals(arg, shortWord);
118 }
119
120 bool Cmdline::parse(vector<string> &parsed)
121 {
122     parsed.clear();
123     if (m_argc) {
124         parsed.push_back(m_argv[0]);
125     }
126     m_delimiter = "\n\n";
127
128     // All command line options which ask for a specific operation,
129     // like --restore, --print-config, ... Used to detect conflicting
130     // operations.
131     vector<string> operations;
132
133     int opt = 1;
134     bool ok;
135     while (opt < m_argc) {
136         parsed.push_back(m_argv[opt]);
137         if (boost::iequals(m_argv[opt], "--")) {
138             // separator between options and <config> <source>:
139             // swallow it and leave option parsing
140             opt++;
141             break;
142         }
143         if (m_argv[opt][0] != '-') {
144             if (strchr(m_argv[opt], '=')) {
145                 // property assignment
146                 if (!parseProp(UNKNOWN_PROPERTY_TYPE,
147                                NULL,
148                                m_argv[opt],
149                                NULL)) {
150                     return false;
151                 } else {
152                     opt++;
153                     continue;
154                 }
155             } else {
156                 break;
157             }
158         }
159         if (IsKeyword(m_argv[opt], "--sync", "-s")) {
160             if (!parseAssignment(opt, parsed, SOURCE_PROPERTY_TYPE, "sync", NULL)) {
161                 return false;
162             }
163
164             // disable requirement to add --run explicitly in order to
165             // be compatible with traditional command lines
166             m_run = true;
167         } else if (IsKeyword(m_argv[opt], "--keyring", "-k")) {
168             if (!parseAssignment(opt, parsed, SYNC_PROPERTY_TYPE, "keyring", "true")) {
169                 return false;
170             }
171         } else if(boost::iequals(m_argv[opt], "--sync-property") ||
172                   boost::iequals(m_argv[opt], "-y")) {
173                 opt++;
174                 if (!parseProp(SYNC_PROPERTY_TYPE,
175                                m_argv[opt - 1], opt == m_argc ? NULL : m_argv[opt])) {
176                     return false;
177                 }
178                 parsed.push_back(m_argv[opt]);
179         } else if(boost::iequals(m_argv[opt], "--source-property") ||
180                   boost::iequals(m_argv[opt], "-z")) {
181             opt++;
182             if (!parseProp(SOURCE_PROPERTY_TYPE,
183                            m_argv[opt - 1], opt == m_argc ? NULL : m_argv[opt])) {
184                 return false;
185             }
186             parsed.push_back(m_argv[opt]);
187         }else if(boost::iequals(m_argv[opt], "--template") ||
188                   boost::iequals(m_argv[opt], "-l")) {
189             opt++;
190             if (opt >= m_argc) {
191                 usage(false, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
192                 return false;
193             }
194             parsed.push_back(m_argv[opt]);
195             m_template = m_argv[opt];
196             m_configure = true;
197             string temp = boost::trim_copy (m_template);
198             if (temp.find ("?") == 0){
199                 m_printTemplates = true;
200                 m_dontrun = true;
201                 m_template = temp.substr (1);
202             }
203         } else if(boost::iequals(m_argv[opt], "--print-databases")) {
204             operations.push_back(m_argv[opt]);
205             m_printDatabases = true;
206         } else if(boost::iequals(m_argv[opt], "--print-servers") ||
207                   boost::iequals(m_argv[opt], "--print-peers") ||
208                   boost::iequals(m_argv[opt], "--print-configs")) {
209             operations.push_back(m_argv[opt]);
210             m_printServers = true;
211         } else if(boost::iequals(m_argv[opt], "--print-config") ||
212                   boost::iequals(m_argv[opt], "-p")) {
213             operations.push_back(m_argv[opt]);
214             m_printConfig = true;
215         } else if(boost::iequals(m_argv[opt], "--print-sessions")) {
216             operations.push_back(m_argv[opt]);
217             m_printSessions = true;
218         } else if(boost::iequals(m_argv[opt], "--configure") ||
219                   boost::iequals(m_argv[opt], "-c")) {
220             operations.push_back(m_argv[opt]);
221             m_configure = true;
222         } else if(boost::iequals(m_argv[opt], "--remove")) {
223             operations.push_back(m_argv[opt]);
224             m_remove = true;
225         } else if(boost::iequals(m_argv[opt], "--run") ||
226                   boost::iequals(m_argv[opt], "-r")) {
227             operations.push_back(m_argv[opt]);
228             m_run = true;
229         } else if(boost::iequals(m_argv[opt], "--restore")) {
230             operations.push_back(m_argv[opt]);
231             opt++;
232             if (opt >= m_argc) {
233                 usage(false, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
234                 return false;
235             }
236             m_restore = m_argv[opt];
237             if (m_restore.empty()) {
238                 usage(false, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
239                 return false;
240             }
241             //if can't convert it successfully, it's an invalid path
242             if (!relToAbs(m_restore)) {
243                 usage(false, string("parameter '") + m_restore + "' for " + cmdOpt(m_argv[opt - 1]) + " must be log directory");
244                 return false;
245             }
246             parsed.push_back(m_restore);
247         } else if(boost::iequals(m_argv[opt], "--before")) {
248             m_before = true;
249         } else if(boost::iequals(m_argv[opt], "--after")) {
250             m_after = true;
251         } else if (boost::iequals(m_argv[opt], "--print-items")) {
252             operations.push_back(m_argv[opt]);
253             m_printItems = m_accessItems = true;
254         } else if ((boost::iequals(m_argv[opt], "--export") && (m_export = true)) ||
255                    (boost::iequals(m_argv[opt], "--import") && (m_import = true)) ||
256                    (boost::iequals(m_argv[opt], "--update") && (m_update = true))) {
257             operations.push_back(m_argv[opt]);
258             m_accessItems = true;
259             opt++;
260             if (opt >= m_argc || !m_argv[opt][0]) {
261                 usage(false, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
262                 return false;
263             }
264             m_itemPath = m_argv[opt];
265             if (m_itemPath != "-") {
266                 string dir, file;
267                 splitPath(m_itemPath, dir, file);
268                 if (dir.empty()) {
269                     dir = ".";
270                 }
271                 if (!relToAbs(dir)) {
272                     SyncContext::throwError(dir, errno);
273                 }
274                 m_itemPath = dir + "/" + file;
275             }
276             parsed.push_back(m_itemPath);
277         } else if (boost::iequals(m_argv[opt], "--delimiter")) {
278             opt++;
279             if (opt >= m_argc) {
280                 usage(false, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
281                 return false;
282             }
283             m_delimiter = m_argv[opt];
284             parsed.push_back(m_delimiter);
285         } else if (boost::iequals(m_argv[opt], "--delete-items")) {
286             operations.push_back(m_argv[opt]);
287             m_deleteItems = m_accessItems = true;
288         } else if(boost::iequals(m_argv[opt], "--dry-run")) {
289             m_dryrun = true;
290         } else if(boost::iequals(m_argv[opt], "--migrate")) {
291             operations.push_back(m_argv[opt]);
292             m_migrate = true;
293         } else if(boost::iequals(m_argv[opt], "--status") ||
294                   boost::iequals(m_argv[opt], "-t")) {
295             operations.push_back(m_argv[opt]);
296             m_status = true;
297         } else if(boost::iequals(m_argv[opt], "--quiet") ||
298                   boost::iequals(m_argv[opt], "-q")) {
299             m_quiet = true;
300         } else if(boost::iequals(m_argv[opt], "--help") ||
301                   boost::iequals(m_argv[opt], "-h")) {
302             m_usage = true;
303         } else if(boost::iequals(m_argv[opt], "--version")) {
304             operations.push_back(m_argv[opt]);
305             m_version = true;
306         } else if (parseBool(opt, "--daemon", NULL, true, m_useDaemon, ok)) {
307             if (!ok) {
308                 return false;
309             }
310         } else if(boost::iequals(m_argv[opt], "--monitor")||
311                 boost::iequals(m_argv[opt], "-m")) {
312             operations.push_back(m_argv[opt]);
313             m_monitor = true;
314         } else if (boost::iequals(m_argv[opt], "--luids")) {
315             // all following parameters are luids; can't be combined
316             // with setting config and source name
317             while (++opt < m_argc) {
318                 parsed.push_back(m_argv[opt]);
319                 m_luids.push_back(CmdlineLUID::toLUID(m_argv[opt]));
320             }
321         } else {
322             usage(false, string(m_argv[opt]) + ": unknown parameter");
323             return false;
324         }
325         opt++;
326     }
327
328     if (opt < m_argc) {
329         m_server = m_argv[opt++];
330         while (opt < m_argc) {
331             parsed.push_back(m_argv[opt]);
332             if (m_sources.empty() ||
333                 !m_accessItems) {
334                 m_sources.insert(m_argv[opt++]);
335             } else {
336                 // first additional parameter was source, rest are luids
337                 m_luids.push_back(CmdlineLUID::toLUID(m_argv[opt++]));
338             }
339         }
340     }
341
342     // check whether we have conflicting operations requested by user
343     if (operations.size() > 1) {
344         usage(false, boost::join(operations, " ") + ": mutually exclusive operations");
345         return false;
346     }
347
348     // common sanity checking for item listing/import/export/update
349     if (m_accessItems) {
350         if ((m_import || m_update) && m_dryrun) {
351             usage(false, operations[0] + ": --dry-run not supported");
352             return false;
353         }
354     }
355
356     return true;
357 }
358
359 bool Cmdline::parseBool(int opt, const char *longName, const char *shortName,
360                         bool def, Bool &value,
361                         bool &ok)
362 {
363     string option = m_argv[opt];
364     string param;
365     size_t pos = option.find('=');
366     if (pos != option.npos) {
367         param = option.substr(pos + 1);
368         option.resize(pos);
369     }
370     if ((longName && boost::iequals(option, longName)) ||
371         (shortName && boost::iequals(option, shortName))) {
372         ok = true;
373         if (param.empty()) {
374             value = def;
375         } else if (boost::iequals(param, "t") ||
376                    boost::iequals(param, "1") ||
377                    boost::iequals(param, "true") ||
378                    boost::iequals(param, "yes")) {
379             value = true;
380         } else if (boost::iequals(param, "f") ||
381               boost::iequals(param, "0") ||
382               boost::iequals(param, "false") ||
383               boost::iequals(param, "no")) {
384             value = false;
385         } else {
386             usage(false, string("parameter in '") + m_argv[opt] + "' must be 1/t/true/yes or 0/f/false/no");
387             ok = false;
388         }
389         // was our option
390         return true;
391     } else {
392         // keep searching for match
393         return false;
394     }
395 }
396
397 bool Cmdline::isSync()
398 {
399     // make sure command line arguments really try to run sync
400     if (m_usage || m_version ||
401         m_printServers || boost::trim_copy(m_server) == "?" ||
402         m_printTemplates || m_dontrun ||
403         m_argc == 1 || (m_useDaemon.wasSet() && m_argc == 2) ||
404         m_printDatabases ||
405         m_printConfig || m_remove ||
406         (m_server == "" && m_argc > 1) ||
407         m_configure || m_migrate ||
408         m_status || m_printSessions ||
409         !m_restore.empty() ||
410         m_accessItems ||
411         m_dryrun ||
412         (!m_run && m_props.hasProperties(FullProps::IGNORE_GLOBAL_PROPS))) {
413         return false;
414     } else {
415         return true;
416     }
417 }
418
419 bool Cmdline::isRestore() const
420 {
421     return !m_restore.empty();
422 }
423
424 bool Cmdline::dontRun() const
425 {
426     // this mimics the if() checks in run()
427     if (m_usage || m_version ||
428         m_printServers || boost::trim_copy(m_server) == "?" ||
429         m_printTemplates) {
430         return false;
431     } else {
432         return m_dontrun;
433     }
434 }
435
436 void Cmdline::makeObsolete(boost::shared_ptr<SyncConfig> &from)
437 {
438     string oldname = from->getRootPath();
439     string newname, suffix;
440     for (int counter = 0; true; counter++) {
441         ostringstream newsuffix;
442         newsuffix << ".old";
443         if (counter) {
444             newsuffix << "." << counter;
445         }
446         suffix = newsuffix.str();
447         newname = oldname + suffix;
448         if (from->hasPeerProperties()) {
449             boost::shared_ptr<SyncConfig> renamed(new SyncConfig(from->getPeerName() + suffix));
450             if (renamed->exists()) {
451                 // don't pick a config name which has the same peer name
452                 // as some other, existing config
453                 continue;
454             }
455         }
456
457         // now renaming should succeed, but let's check anyway
458         if (!rename(oldname.c_str(),
459                     newname.c_str())) {
460             break;
461         } else if (errno != EEXIST && errno != ENOTEMPTY) {
462             SE_THROW(StringPrintf("renaming %s to %s: %s",
463                                   oldname.c_str(),
464                                   newname.c_str(),
465                                   strerror(errno)));
466         }
467     }
468
469     string newConfigName;
470     string oldContext = from->getContextName();
471     if (from->hasPeerProperties()) {
472         newConfigName = from->getPeerName() + suffix + oldContext;
473     } else {
474         newConfigName = oldContext + suffix;
475     }
476     from.reset(new SyncConfig(newConfigName));
477 }
478
479 void Cmdline::copyConfig(const boost::shared_ptr<SyncConfig> &from,
480                          const boost::shared_ptr<SyncConfig> &to,
481                          const set<string> &selectedSources)
482 {
483     const set<string> *sources = NULL;
484     set<string> allSources;
485     if (!selectedSources.empty()) {
486         // use explicitly selected sources
487         sources = &selectedSources;
488     } else {
489         // need an explicit list of all sources which will be copied,
490         // for the createFilters() call below
491         BOOST_FOREACH(const std::string &source, from->getSyncSources()) {
492             allSources.insert(source);
493         }
494         sources = &allSources;
495     }
496
497     // Apply config changes on-the-fly. Regardless what we do
498     // (changing an existing config, migrating, creating from
499     // a template), existing shared properties in the desired
500     // context must be preserved unless explicitly overwritten.
501     // Therefore read those, update with command line properties,
502     // then set as filter.
503     ConfigProps syncFilter;
504     SourceProps sourceFilters;
505     m_props.createFilters(to->getContextName(), to->getConfigName(), sources, syncFilter, sourceFilters);
506     from->setConfigFilter(true, "", syncFilter);
507     BOOST_FOREACH(const SourceProps::value_type &entry, sourceFilters) {
508         from->setConfigFilter(false, entry.first, entry.second);
509     }
510
511     // Modify behavior of target UI before using it: set keyring
512     // property separately.
513     InitStateTri keyring = from->getKeyring();
514     if (keyring.wasSet()) {
515         to->setKeyring(keyring);
516     }
517
518     // Write into the requested configuration, creating it if necessary.
519     to->prepareConfigForWrite();
520     to->copy(*from, sources);
521 }
522
523 void Cmdline::finishCopy(const boost::shared_ptr<SyncConfig> &from,
524                          const boost::shared_ptr<SyncContext> &to)
525 {
526     // give a change to do something before flushing configs to files
527     to->preFlush(to->getUserInterfaceNonNull());
528
529     // done, now write it
530     m_configModified = true;
531     to->flush();
532
533     // migrating peer?
534     if (m_migrate &&
535         from->hasPeerProperties()) {
536         
537         // also copy .synthesis dir
538         string fromDir, toDir;
539         fromDir = from->getRootPath() + "/.synthesis";
540         toDir = to->getRootPath() + "/.synthesis";
541         if (isDir(fromDir)) {
542             cp_r(fromDir, toDir);
543         }
544
545         // Succeeded so far, remove "ConsumerReady" flag from migrated
546         // config to hide that old config from normal UI users. Must
547         // do this without going through SyncConfig, because that
548         // would bump the version.
549         BoolConfigProperty ready("ConsumerReady", "", "0");
550         // Also disable auto-syncing in the migrated config.
551         StringConfigProperty autosync("autoSync", "", "");
552         {
553             IniFileConfigNode node(from->getRootPath(), "config.ini", false);
554             if (ready.getPropertyValue(node)) {
555                 ready.setProperty(node, false);
556             }
557             if (!autosync.getProperty(node).empty()) {
558                 autosync.setProperty(node, InitStateString("0", true));
559             }
560             node.flush();
561         }
562
563         // same for very old configs
564         {
565             IniFileConfigNode node(from->getRootPath() + "/spds/syncml", "config.txt", false);
566             if (!autosync.getProperty(node).empty()) {
567                 autosync.setProperty(node, InitStateString("0", true));
568             }
569             node.flush();
570         }
571
572         // Set ConsumerReady for migrated SyncEvolution < 1.2
573         // configs, because in older releases all existing
574         // configurations where shown. SyncEvolution 1.2 is more
575         // strict and assumes that ConsumerReady must be set
576         // explicitly. The sync-ui always has set the flag for
577         // configs created or modified with it, but the command
578         // line did not. Matches similar code in
579         // syncevo-dbus-server.          
580         if (from->getConfigVersion(CONFIG_LEVEL_PEER, CONFIG_CUR_VERSION) == 0 /* SyncEvolution < 1.2 */) {
581             to->setConsumerReady(true);
582             to->flush();
583         }
584     }
585 }
586
587 void Cmdline::migratePeer(const std::string &fromPeer, const std::string &toPeer)
588 {
589     boost::shared_ptr<SyncConfig> from(new SyncConfig(fromPeer));
590     makeObsolete(from);
591     // hack: move to different target config for createSyncClient()
592     m_server = toPeer;
593     boost::shared_ptr<SyncContext> to(createSyncClient());
594
595     // Special case for Memotoo: explicitly set preferred sync format
596     // to vCard 3.0 as part of the SyncEvolution 1.1.x -> 1.2 migration,
597     // because it works better. Template was also updated in 1.2, but
598     // that alone wouldn't improve existing configs.
599     if (from->getConfigVersion(CONFIG_LEVEL_PEER, CONFIG_CUR_VERSION) == 0) {
600         vector<string> urls = from->getSyncURL();
601         if (urls.size() == 1 &&
602             urls[0] == "http://sync.memotoo.com/syncML") {
603             boost::shared_ptr<SyncContext> to(createSyncClient());
604             m_props[to->getContextName()].m_sourceProps["addressbook"].insert(make_pair("syncFormat", "text/vcard"));
605         }
606     }
607
608     copyConfig(from, to, set<string>());
609     finishCopy(from, to);
610 }
611
612 /**
613  * Finds first instance of delimiter string in other string. In
614  * addition, it treats "\n\n" in a special way: that delimiter also
615  * matches "\n\r\n".
616  */
617 class FindDelimiter {
618     const string m_delimiter;
619 public:
620     FindDelimiter(const string &delimiter) :
621         m_delimiter(delimiter)
622     {}
623     boost::iterator_range<string::iterator> operator()(string::iterator begin,
624                                                        string::iterator end)
625     {
626         if (m_delimiter == "\n\n") {
627             // match both "\n\n" and "\n\r\n"
628             while (end - begin >= 2) {
629                 if (*begin == '\n') {
630                     if (*(begin + 1) == '\n') {
631                         return boost::iterator_range<string::iterator>(begin, begin + 2);
632                     } else if (end - begin >= 3 &&
633                                *(begin + 1) == '\r' &&
634                                *(begin + 2) == '\n') {
635                         return boost::iterator_range<string::iterator>(begin, begin + 3);
636                     }
637                 }
638                 ++begin;
639             }
640             return boost::iterator_range<string::iterator>(end, end);
641         } else {
642             boost::sub_range<string> range(begin, end);
643             return boost::find_first(range, m_delimiter);
644         }
645     }
646 };
647
648 bool Cmdline::run() {
649     // --dry-run is only supported by some operations.
650     // Be very strict about it and make sure it is off in all
651     // potentially harmful operations, otherwise users might
652     // expect it to have an effect when it doesn't.
653
654     // TODO: check filter properties for invalid config and source
655     // names
656
657     if (m_usage) {
658         usage(true);
659     } else if (m_version) {
660         SE_LOG_SHOW(NULL, NULL, "SyncEvolution %s%s\n%s%s",
661                     VERSION,
662                     SyncContext::isStableRelease() ? "" : " (pre-release)",
663                     EDSAbiWrapperInfo(),
664                     SyncSource::backendsInfo().c_str());
665     } else if (m_printServers || boost::trim_copy(m_server) == "?") {
666         dumpConfigs("Configured servers:",
667                     SyncConfig::getConfigs());
668     } else if (m_printTemplates) {
669         SyncConfig::DeviceList devices;
670         if (m_template.empty()){
671             devices.push_back (SyncConfig::DeviceDescription("", "", SyncConfig::MATCH_FOR_CLIENT_MODE));
672             dumpConfigTemplates("Available configuration templates (servers):",
673                     SyncConfig::getPeerTemplates(devices), false);
674         } else {
675             //limiting at templates for syncml clients only.
676             devices.push_back (SyncConfig::DeviceDescription("", m_template, SyncConfig::MATCH_FOR_SERVER_MODE));
677             dumpConfigTemplates("Available configuration templates (clients):",
678                     SyncConfig::matchPeerTemplates(devices), true);
679         }
680     } else if (m_dontrun) {
681         // user asked for information
682     } else if (m_printDatabases) {
683         // list databases
684         const SourceRegistry &registry(SyncSource::getSourceRegistry());
685         boost::shared_ptr<SyncSourceNodes> nodes;
686         std::string header;
687         boost::shared_ptr<SyncContext> context;
688         FilterConfigNode::ConfigFilter sourceFilter = m_props.createSourceFilter(m_server, "");
689         FilterConfigNode::ConfigFilter::const_iterator backend = sourceFilter.find("backend");
690
691         if (!m_server.empty()) {
692             // list for specific backend chosen via config
693             if (m_sources.size() != 1) {
694                 SE_THROW(StringPrintf("must specify exactly one source after the config name '%s'",
695                                       m_server.c_str()));
696             }
697             context.reset(new SyncContext(m_server));
698             if (!context->exists()) {
699                 SE_THROW(StringPrintf("config '%s' does not exist", m_server.c_str()));
700             }
701             nodes.reset(new SyncSourceNodes(context->getSyncSourceNodesNoTracking(*m_sources.begin())));
702             header = StringPrintf("%s/%s", m_server.c_str(), m_sources.begin()->c_str());
703             if (!nodes->dataConfigExists()) {
704                 SE_THROW(StringPrintf("%s does not exist",
705                                       header.c_str()));
706             }
707         } else {
708             context.reset(new SyncContext);
709             boost::shared_ptr<FilterConfigNode> sharedNode(new VolatileConfigNode());
710             boost::shared_ptr<FilterConfigNode> configNode(new VolatileConfigNode());
711             boost::shared_ptr<FilterConfigNode> hiddenNode(new VolatileConfigNode());
712             boost::shared_ptr<FilterConfigNode> trackingNode(new VolatileConfigNode());
713             boost::shared_ptr<FilterConfigNode> serverNode(new VolatileConfigNode());
714             nodes.reset(new SyncSourceNodes(true, sharedNode, configNode, hiddenNode, trackingNode, serverNode, ""));
715             header = backend != sourceFilter.end() ?
716                 backend->second :
717                 "???";
718         }
719         nodes->getProperties()->setFilter(sourceFilter);
720         FilterConfigNode::ConfigFilter syncFilter = m_props.createSyncFilter(m_server);
721         context->setConfigFilter(true, "", syncFilter);
722
723         SyncSourceParams params("list", *nodes, context);
724         if (!m_server.empty() || backend != sourceFilter.end()) {
725             // list for specific backend
726             auto_ptr<SyncSource> source(SyncSource::createSource(params, false, NULL));
727             if (source.get() != NULL) {
728                 listSources(*source, header);
729                 SE_LOG_SHOW(NULL, NULL, "\n");
730             } else {
731                 SE_LOG_SHOW(NULL, NULL, "%s:\n   cannot list databases", header.c_str());
732             }
733         } else {
734             // list for all backends
735             BOOST_FOREACH(const RegisterSyncSource *source, registry) {
736                 BOOST_FOREACH(const Values::value_type &alias, source->m_typeValues) {
737                     if (!alias.empty() && source->m_enabled) {
738                         SourceType type(*alias.begin());
739                         nodes->getProperties()->setProperty("backend", type.m_backend);
740                         std::string header = boost::join(alias, " = ");
741                         try {
742                             auto_ptr<SyncSource> source(SyncSource::createSource(params, false));
743                             if (!source.get()) {
744                                 // silently skip backends like the "file" backend which do not support
745                                 // listing databases and return NULL unless configured properly
746                             } else {
747                                 listSources(*source, header);
748                                 SE_LOG_SHOW(NULL, NULL, "\n");
749                             }
750                         } catch (...) {
751                             SE_LOG_ERROR(NULL, NULL, "%s:\nlisting databases failed", header.c_str());
752                             Exception::handle();
753                         }
754                     }
755                 }
756             }
757         }
758     } else if (m_printConfig) {
759         boost::shared_ptr<SyncConfig> config;
760         ConfigProps syncFilter;
761         SourceProps sourceFilters;
762
763         if (m_template.empty()) {
764             if (m_server.empty()) {
765                 usage(false, "--print-config requires either a --template or a server name.");
766                 return false;
767             }
768             config.reset(new SyncConfig(m_server));
769             if (!config->exists()) {
770                 SE_LOG_ERROR(NULL, NULL, "Server '%s' has not been configured yet.", m_server.c_str());
771                 return false;
772             }
773
774             // No need to include a context or additional sources,
775             // because reading the m_server config already includes
776             // the right information.
777             m_props.createFilters("", m_server, NULL, syncFilter, sourceFilters);
778         } else {
779             string peer, context;
780             SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(m_template,
781                                                                             SyncConfig::NormalizeFlags(SyncConfig::NORMALIZE_SHORTHAND|SyncConfig::NORMALIZE_IS_NEW)),
782                                           peer, context);
783             config = SyncConfig::createPeerTemplate(peer);
784             if (!config.get()) {
785                 SE_LOG_ERROR(NULL, NULL, "No configuration template for '%s' available.", m_template.c_str());
786                 return false;
787             }
788
789             // When instantiating a template, include the properties
790             // of the target context as filter to preserve shared
791             // properties, the final name inside that context as
792             // peer config name, and the sources defined in the template.
793             list<string> sourcelist = config->getSyncSources();
794             set<string> sourceset(sourcelist.begin(), sourcelist.end());
795             m_props.createFilters(std::string("@") + context, "",
796                                   &sourceset,
797                                   syncFilter, sourceFilters);
798         }
799
800         // determine whether we dump a peer or a context
801         int flags = DUMP_PROPS_NORMAL;
802         string peer, context;
803         SyncConfig::splitConfigString(config->getConfigName(), peer, context);
804         if (peer.empty()) {
805             flags |= HIDE_PER_PEER;
806             checkForPeerProps();
807         } 
808
809         if (m_sources.empty() ||
810             m_sources.find("main") != m_sources.end()) {
811             boost::shared_ptr<FilterConfigNode> syncProps(config->getProperties());
812             syncProps->setFilter(syncFilter);
813             dumpProperties(*syncProps, config->getRegistry(), flags);
814         }
815
816         list<string> sources = config->getSyncSources();
817         sources.sort();
818         BOOST_FOREACH(const string &name, sources) {
819             if (m_sources.empty() ||
820                 m_sources.find(name) != m_sources.end()) {
821                 SE_LOG_SHOW(NULL, NULL, "[%s]", name.c_str());
822                 SyncSourceNodes nodes = config->getSyncSourceNodes(name);
823                 boost::shared_ptr<FilterConfigNode> sourceProps = nodes.getProperties();
824                 sourceProps->setFilter(sourceFilters.createSourceFilter(name));
825                 dumpProperties(*sourceProps, SyncSourceConfig::getRegistry(),
826                                flags | ((name != *(--sources.end())) ? HIDE_LEGEND : DUMP_PROPS_NORMAL));
827             }
828         }
829     } else if (m_configure || m_migrate) {
830         if (!needConfigName()) {
831             return false;
832         }
833         if (m_dryrun) {
834             SyncContext::throwError("--dry-run not supported for configuration changes");
835         }
836
837         // name of renamed config ("foo.old") after migration
838         string newname;
839
840         // True if the target configuration is a context like @default
841         // or @foobar. Relevant in several places in the following
842         // code.
843         bool configureContext = false;
844
845         bool fromScratch = false;
846         string peer, context;
847         SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(m_server), peer, context);
848         if (peer.empty()) {
849             configureContext = true;
850             checkForPeerProps();
851         }
852
853         // Make m_server a fully-qualified name. Useful in error
854         // messages and essential for migrating "foo" where "foo"
855         // happens to map to "foo@bar".  Otherwise "foo" will be
856         // mapped incorrectly to "foo@default" after renaming
857         // "foo@bar" to "foo.old@bar".
858         //
859         // The inverse problem can occur for "foo@default": after
860         // renaming, "foo" without "@default" would be mapped to
861         // "foo@somewhere-else" if such a config exists.
862         m_server = peer + "@" + context;
863
864         // Both config changes and migration are implemented as copying from
865         // another config (template resp. old one). Migration also moves
866         // the old config. The target configuration is determined by m_server,
867         // but the exact semantic of it depends on the operation.
868         boost::shared_ptr<SyncConfig> from;
869         boost::shared_ptr<SyncContext> to;
870         string origPeer;
871         if (m_migrate) {
872             if (!m_sources.empty()) {
873                 SE_LOG_ERROR(NULL, NULL, "cannot migrate individual sources");
874                 return false;
875             }
876
877             string oldContext = context;
878             from.reset(new SyncConfig(m_server));
879             if (!from->exists()) {
880                 // for migration into a different context, search for config without context
881                 oldContext = "";
882                 from.reset(new SyncConfig(peer));
883                 if (!from->exists()) {
884                     SE_LOG_ERROR(NULL, NULL, "Server '%s' has not been configured yet.", m_server.c_str());
885                     return false;
886                 }
887             }
888
889             // Check if we are migrating an individual peer inside
890             // a context which itself is too old. In that case,
891             // the whole context and everything inside it needs to
892             // be migrated.
893             if (!configureContext) {
894                 bool obsoleteContext = false;
895                 if (from->getLayout() < SyncConfig::SHARED_LAYOUT) {
896                     // check whether @default context exists and is too old;
897                     // in that case migrate it first
898                     SyncConfig target("@default");
899                     if (target.exists() &&
900                         target.getConfigVersion(CONFIG_LEVEL_CONTEXT, CONFIG_CUR_VERSION) <
901                         CONFIG_CONTEXT_MIN_VERSION) {
902                         // migrate all peers inside @default *and* the one outside
903                         origPeer = m_server;
904                         m_server = "@default";
905                         obsoleteContext = true;
906                     }
907                 } else {
908                     // config already is inside a context; need to check that context
909                     if (from->getConfigVersion(CONFIG_LEVEL_CONTEXT, CONFIG_CUR_VERSION) <
910                         CONFIG_CONTEXT_MIN_VERSION) {
911                         m_server = string("@") + context;
912                         obsoleteContext = true;
913                     }
914                 }
915                 if (obsoleteContext) {
916                     // hack: move to different config and back later
917                     from.reset(new SyncConfig(m_server));
918                     peer = "";
919                     configureContext = true;
920                 }
921             }
922
923             // rename on disk and point "from" to it
924             makeObsolete(from);
925
926             // modify the config referenced by the (possibly modified) m_server
927             to.reset(createSyncClient());
928         } else {
929             from.reset(new SyncConfig(m_server));
930             // m_server known, modify it
931             to.reset(createSyncClient());
932
933             if (!from->exists()) {
934                 // creating from scratch, look for template
935                 fromScratch = true;
936                 string configTemplate;
937                 if (m_template.empty()) {
938                     if (configureContext) {
939                         // configuring a context, template doesn't matter =>
940                         // use default "SyncEvolution" template
941                         configTemplate =
942                             peer = "SyncEvolution";
943                         from = SyncConfig::createPeerTemplate(peer);
944                     } else if (peer == "target-config") {
945                         // Configuring the source context for local sync
946                         // => determine template based on context name.
947                         configTemplate = context;
948                         from = SyncConfig::createPeerTemplate(context);
949                     } else {
950                         // template is the peer name
951                         configTemplate = m_server;
952                         from = SyncConfig::createPeerTemplate(peer);
953                     }
954                 } else {
955                     // Template is specified explicitly. It must not contain a context,
956                     // because the context comes from the config name.
957                     configTemplate = m_template;
958                     if (SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(configTemplate,
959                                                                                         SyncConfig::NormalizeFlags(SyncConfig::NORMALIZE_SHORTHAND|SyncConfig::NORMALIZE_IS_NEW)),
960                                                       peer, context)) {
961                         SE_LOG_ERROR(NULL, NULL, "Template %s must not specify a context.", configTemplate.c_str());
962                         return false;
963                     }
964                     string tmp;
965                     SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(m_server), tmp, context);
966                     from = SyncConfig::createPeerTemplate(peer);
967                 }
968                 list<string> missing;
969                 if (!from.get()) {
970                     // check if all obligatory sync properties are specified; needed
971                     // for both the "is complete" check and the error message below
972                     ConfigProps syncProps = m_props.createSyncFilter(to->getContextName());
973                     bool complete = true;
974                     BOOST_FOREACH(const ConfigProperty *prop, SyncConfig::getRegistry()) {
975                         if (prop->isObligatory() &&
976                             syncProps.find(prop->getMainName()) == syncProps.end()) {
977                             missing.push_back(prop->getMainName());
978                             complete = false;
979                         }
980                     }
981
982                     // if everything was specified and no invalid template name was given, allow user
983                     // to proceed with "none" template; if a template was specified, we skip
984                     // this and go directly to the code below which prints an error message
985                     if (complete &&
986                         m_template.empty()) {
987                         from = SyncConfig::createPeerTemplate("none");
988                     }
989                 }
990                 if (!from.get()) {
991                     SE_LOG_ERROR(NULL, NULL, "No configuration template for '%s' available.", configTemplate.c_str());
992                     if (m_template.empty()) {
993                         SE_LOG_INFO(NULL, NULL,
994                                     "Use '--template none' and/or specify relevant properties on the command line to create a configuration without a template. Need values for: %s",
995                                     boost::join(missing, ", ").c_str());
996                     } else if (missing.empty()) {
997                         SE_LOG_INFO(NULL, NULL, "All relevant properties seem to be set, omit the --template parameter to proceed.");
998                     }
999                     SE_LOG_INFO(NULL, NULL, "\n");
1000                     SyncConfig::DeviceList devices;
1001                     devices.push_back(SyncConfig::DeviceDescription("", "", SyncConfig::MATCH_ALL));
1002                     dumpConfigTemplates("Available configuration templates (clients and servers):",
1003                                         SyncConfig::getPeerTemplates(devices),
1004                                         false,
1005                                         Logger::INFO);
1006                     return false;
1007                 }
1008             }
1009         }
1010
1011         // Which sources are configured is determined as follows:
1012         // - all sources in the template by default (empty set), except when
1013         // - sources are listed explicitly, and either
1014         // - updating an existing config or
1015         // - configuring a context.
1016         //
1017         // This implies that when configuring a peer from scratch, all
1018         // sources in the template will be created, with command line
1019         // source properties applied to all of them. This might not be
1020         // what we want, but because this is how we have done it
1021         // traditionally, I keep this behavior for now.
1022         //
1023         // When migrating, m_sources is empty and thus the whole set of
1024         // sources will be migrated. Checking it here for clarity's sake.
1025         set<string> sources;
1026         if (!m_migrate &&
1027             !m_sources.empty() &&
1028             (!fromScratch || configureContext)) {
1029             sources = m_sources;
1030         }
1031
1032         // Also copy (aka create) sources listed on the command line if
1033         // creating from scratch and
1034         // - "--template none" enables the "do what I want" mode or
1035         // - source properties apply to it.
1036         // Creating from scratch with other sources is a possible typo
1037         // and will trigger an error below.
1038         if (fromScratch) {
1039             BOOST_FOREACH(const string &source, m_sources) {
1040                 if (m_template == "none" ||
1041                     !m_props.createSourceFilter(to->getContextName(), source).empty()) {
1042                     sources.insert(source);
1043                 }
1044             }
1045         }
1046
1047         // Special case for migration away from "type": older
1048         // SyncEvolution could cope with "type" only set correctly for
1049         // peers. Real-world case: Memotoo config, context had "type =
1050         // calendar" set for address book.
1051         //
1052         // Setting "backend" based on an incorrect "type" from the
1053         // context would lead to a broken, unusable config. Solution:
1054         // take "backend" and "databaseFormat" from a peer config when
1055         // migrating a context.
1056         //
1057         // Note that peers are assumed to be consistent. Not attempt is
1058         // made to detect a config which has inconsistent peer configs.
1059         if (m_migrate && configureContext &&
1060             from->getConfigVersion(CONFIG_LEVEL_CONTEXT, CONFIG_CUR_VERSION) == 0) {
1061             list<string> peers = from->getPeers();
1062             peers.sort(); // make code below deterministic
1063
1064             BOOST_FOREACH(const std::string source, from->getSyncSources()) {
1065                 BOOST_FOREACH(const string &peer, peers) {
1066                     IniFileConfigNode node(from->getRootPath() + "/peers/" + peer + "/sources/" + source,
1067                                            "config.ini",
1068                                            true);
1069                     string sync = node.readProperty("sync");
1070                     if (sync.empty() ||
1071                         boost::iequals(sync, "none") ||
1072                         boost::iequals(sync, "disabled")) {
1073                         // ignore this peer, it doesn't use the source
1074                         continue;
1075                     }
1076
1077                     SourceType type(node.readProperty("type"));
1078                     if (!type.m_backend.empty()) {
1079                         // found some "type": use "backend" and
1080                         // "dataFormat" in filter, unless the user
1081                         // already set a value there
1082                         ConfigProps syncFilter;
1083                         SourceProps sourceFilters;
1084                         set<string> set;
1085                         set.insert(source);
1086                         m_props.createFilters(to->getContextName(), "",
1087                                               &set, syncFilter, sourceFilters);
1088                         const ConfigProps &sourceFilter = sourceFilters[source];
1089                         if (sourceFilter.find("backend") == sourceFilter.end()) {
1090                             m_props[to->getContextName()].m_sourceProps[source]["backend"] = type.m_backend;
1091                         }
1092                         if (!type.m_localFormat.empty() &&
1093                             sourceFilter.find("databaseFormat") == sourceFilter.end()) {
1094                             m_props[to->getContextName()].m_sourceProps[source]["databaseFormat"] = type.m_localFormat;
1095                         }
1096                         // use it without bothering to keep looking
1097                         // (no consistenty check!)
1098                         break;
1099                     }
1100                 }
1101             }
1102         }
1103
1104         // copy and filter into the target config: createSyncClient()
1105         // creates a SyncContext for m_server, with propert
1106         // implementation of the password handling methods in derived
1107         // classes (D-Bus server, real command line)
1108         copyConfig(from, to, sources);
1109
1110         // Sources are active now according to the server default.
1111         // Disable all sources not selected by user (if any selected)
1112         // and those which have no database.
1113         if (fromScratch) {
1114             list<string> configuredSources = to->getSyncSources();
1115             set<string> sources = m_sources;
1116             
1117             BOOST_FOREACH(const string &source, configuredSources) {
1118                 boost::shared_ptr<PersistentSyncSourceConfig> sourceConfig(to->getSyncSourceConfig(source));
1119                 string disable = "";
1120                 set<string>::iterator entry = sources.find(source);
1121                 bool selected = entry != sources.end();
1122
1123                 if (!m_sources.empty() &&
1124                     !selected) {
1125                     disable = "not selected";
1126                 } else {
1127                     if (entry != sources.end()) {
1128                         // The command line parameter matched a valid source.
1129                         // All entries left afterwards must have been typos.
1130                         sources.erase(entry);
1131                     }
1132
1133                     // check whether the sync source works
1134                     SyncSourceParams params(source, to->getSyncSourceNodes(source), to);
1135                     auto_ptr<SyncSource> syncSource(SyncSource::createSource(params, false, to.get()));
1136                     if (syncSource.get() == NULL) {
1137                         disable = "no backend available";
1138                     } else {
1139                         try {
1140                             SyncSource::Databases databases = syncSource->getDatabases();
1141                             if (databases.empty()) {
1142                                 disable = "no database to synchronize";
1143                             }
1144                         } catch (...) {
1145                             disable = "backend failed";
1146                         }
1147                     }
1148                 }
1149
1150                 // Do sanity checking of source (can it be enabled?),
1151                 // but only set the sync mode if configuring a peer.
1152                 // A context-only config doesn't have the "sync"
1153                 // property.
1154                 string syncMode;
1155                 if (!disable.empty()) {
1156                     // abort if the user explicitly asked for the sync source
1157                     // and it cannot be enabled, otherwise disable it silently
1158                     if (selected) {
1159                         SyncContext::throwError(source + ": " + disable);
1160                     }
1161                     syncMode = "disabled";
1162                 } else if (selected) {
1163                     // user absolutely wants it: enable even if off by default
1164                     ConfigProps filter = m_props.createSourceFilter(m_server, source);
1165                     ConfigProps::const_iterator sync = filter.find("sync");
1166                     syncMode = sync == filter.end() ? "two-way" : sync->second;
1167                 }
1168                 if (!syncMode.empty() &&
1169                     !configureContext) {
1170                     sourceConfig->setSync(syncMode);
1171                 }
1172             }
1173
1174             if (!sources.empty()) {
1175                 SyncContext::throwError(string("no such source(s): ") + boost::join(sources, " "));
1176             }
1177         }
1178
1179         // flush, move .synthesis dir, set ConsumerReady, ...
1180         finishCopy(from, to);
1181
1182         // Now also migrate all peers inside context?
1183         if (configureContext && m_migrate) {
1184             BOOST_FOREACH(const string &peer, from->getPeers()) {
1185                 migratePeer(peer + from->getContextName(), peer + to->getContextName());
1186             }
1187             if (!origPeer.empty()) {
1188                 migratePeer(origPeer, origPeer + to->getContextName());
1189             }
1190         }
1191     } else if (m_remove) {
1192         if (!needConfigName()) {
1193             return false;
1194         }
1195         if (m_dryrun) {
1196             SyncContext::throwError("--dry-run not supported for removing configurations");
1197         }
1198
1199         // extra sanity check
1200         if (!m_sources.empty() ||
1201             m_props.hasProperties(FullProps::IGNORE_GLOBAL_PROPS)) {
1202             usage(false, "too many parameters for --remove");
1203             return false;
1204         } else {
1205             boost::shared_ptr<SyncConfig> config;
1206             config.reset(new SyncConfig(m_server));
1207             if (!config->exists()) {
1208                 SyncContext::throwError(string("no such configuration: ") + m_server);
1209             }
1210             config->remove();
1211             m_configModified = true;
1212             return true;
1213         }
1214     } else if (m_accessItems) {
1215         // need access to specific source
1216         boost::shared_ptr<SyncContext> context;
1217         context.reset(createSyncClient());
1218
1219         // operating on exactly one source (can be optional)
1220         string sourceName;
1221         bool haveSourceName = !m_sources.empty();
1222         if (haveSourceName) {
1223             sourceName = *m_sources.begin();
1224         }
1225
1226         // apply filters
1227         context->setConfigFilter(true, "", m_props.createSyncFilter(m_server));
1228         context->setConfigFilter(false, "", m_props.createSourceFilter(m_server, sourceName));
1229
1230         SyncSourceNodes sourceNodes = context->getSyncSourceNodesNoTracking(sourceName);
1231         SyncSourceParams params(sourceName, sourceNodes, context);
1232         cxxptr<SyncSource> source;
1233
1234         try {
1235             source.set(SyncSource::createSource(params, true));
1236         } catch (const StatusException &ex) {
1237             // Creating the source failed. Detect some common reasons for this
1238             // and log those instead. None of these situations are fatal by themselves,
1239             // but in combination they are a problem.
1240             if (ex.syncMLStatus() == SyncMLStatus(sysync::LOCERR_CFGPARSE)) {
1241                 std::list<std::string> explanation;
1242
1243                 explanation.push_back(ex.what());
1244                 if (!m_server.empty() && !context->exists()) {
1245                     explanation.push_back(StringPrintf("configuration '%s' does not exist", m_server.c_str()));
1246                 }
1247                 if (haveSourceName && !sourceNodes.exists()) {
1248                     explanation.push_back(StringPrintf("source '%s' does not exist", sourceName.c_str()));
1249                 } else if (!haveSourceName) {
1250                     explanation.push_back("no source selected");
1251                 }
1252                 SyncSourceConfig sourceConfig(sourceName, sourceNodes);
1253                 if (!sourceConfig.getBackend().wasSet()) {
1254                     explanation.push_back("backend property not set");
1255                 }
1256                 SyncContext::throwError(SyncMLStatus(sysync::LOCERR_CFGPARSE),
1257                                         boost::join(explanation, "\n"));
1258             } else {
1259                 throw;
1260             }
1261         }
1262
1263         sysync::TSyError err;
1264 #define CHECK_ERROR(_op) if (err) { SE_THROW_EXCEPTION_STATUS(StatusException, string(source->getName()) + ": " + (_op), SyncMLStatus(err)); }
1265
1266         // acquire passwords before doing anything (interactive password
1267         // access not supported for the command line)
1268         {
1269             ConfigPropertyRegistry& registry = SyncConfig::getRegistry();
1270             BOOST_FOREACH(const ConfigProperty *prop, registry) {
1271                 prop->checkPassword(context->getUserInterfaceNonNull(), m_server, *context->getProperties());
1272             }
1273         }
1274         {
1275             ConfigPropertyRegistry &registry = SyncSourceConfig::getRegistry();
1276             BOOST_FOREACH(const ConfigProperty *prop, registry) {
1277                 prop->checkPassword(context->getUserInterfaceNonNull(), m_server, *context->getProperties(),
1278                                     source->getName(), sourceNodes.getProperties());
1279             }
1280         }
1281
1282         source->open();
1283         const SyncSource::Operations &ops = source->getOperations();
1284         if (m_printItems) {
1285             SyncSourceLogging *logging = dynamic_cast<SyncSourceLogging *>(source.get());
1286             if (!ops.m_startDataRead ||
1287                 !ops.m_readNextItem) {
1288                 source->throwError("reading items not supported");
1289             }
1290
1291             err = ops.m_startDataRead(*source, "", "");
1292             CHECK_ERROR("reading items");
1293             list<string> luids;
1294             readLUIDs(source, luids);
1295             BOOST_FOREACH(string &luid, luids) {
1296                 string description;
1297                 if (logging) {
1298                     description = logging->getDescription(luid);
1299                 }
1300                 SE_LOG_SHOW(NULL, NULL, "%s%s%s",
1301                             CmdlineLUID::fromLUID(luid).c_str(),
1302                             description.empty() ? "" : ": ",
1303                             description.c_str());
1304             }
1305         } else if (m_deleteItems) {
1306             if (!ops.m_deleteItem) {
1307                 source->throwError("deleting items not supported");
1308             }
1309             list<string> luids;
1310             bool deleteAll = std::find(m_luids.begin(), m_luids.end(), "*") != m_luids.end();
1311             err = ops.m_startDataRead(*source, "", "");
1312             CHECK_ERROR("reading items");
1313             if (deleteAll) {
1314                 readLUIDs(source, luids);
1315             } else {
1316                 luids = m_luids;
1317             }
1318             if (ops.m_endDataRead) {
1319                 err = ops.m_endDataRead(*source);
1320                 CHECK_ERROR("stop reading items");
1321             }
1322             if (ops.m_startDataWrite) {
1323                 err = ops.m_startDataWrite(*source);
1324                 CHECK_ERROR("writing items");
1325             }
1326             BOOST_FOREACH(const string &luid, luids) {
1327                 sysync::ItemIDType id;
1328                 id.item = (char *)luid.c_str();
1329                 err = ops.m_deleteItem(*source, &id);
1330                 CHECK_ERROR("deleting item");
1331             }
1332             char *token;
1333             err = ops.m_endDataWrite(*source, true, &token);
1334             if (token) {
1335                 free(token);
1336             }
1337             CHECK_ERROR("stop writing items");
1338         } else {
1339             SyncSourceRaw *raw = dynamic_cast<SyncSourceRaw *>(source.get());
1340             if (!raw) {
1341                 source->throwError("reading/writing items directly not supported");
1342             }
1343             if (m_import || m_update) {
1344                 err = ops.m_startDataRead(*source, "", "");
1345                 CHECK_ERROR("reading items");
1346                 if (ops.m_endDataRead) {
1347                     err = ops.m_endDataRead(*source);
1348                     CHECK_ERROR("stop reading items");
1349                 }
1350                 if (ops.m_startDataWrite) {
1351                     err = ops.m_startDataWrite(*source);
1352                     CHECK_ERROR("writing items");
1353                 }
1354
1355                 cxxptr<ifstream> inFile;
1356                 if (m_itemPath =="-" ||
1357                     !isDir(m_itemPath)) {
1358                     string content;
1359                     string luid;
1360                     if (m_itemPath == "-") {
1361                         context->getUserInterfaceNonNull().readStdin(content);
1362                     } else if (!ReadFile(m_itemPath, content)) {
1363                         SyncContext::throwError(m_itemPath, errno);
1364                     }
1365                     if (m_delimiter == "none") {
1366                         if (m_update) {
1367                             if (m_luids.size() != 1) {
1368                                 SyncContext::throwError("need exactly one LUID parameter");
1369                             } else {
1370                                 luid = *m_luids.begin();
1371                             }
1372                         }
1373                         SE_LOG_SHOW(NULL, NULL, "#0: %s",
1374                                     insertItem(raw, luid, content).getEncoded().c_str());
1375                     } else {
1376                         typedef boost::split_iterator<string::iterator> string_split_iterator;
1377                         int count = 0;
1378                         FindDelimiter finder(m_delimiter);
1379
1380                         // when updating, check number of luids in advance
1381                         if (m_update) {
1382                             unsigned long total = 0;
1383                             for (string_split_iterator it =
1384                                      boost::make_split_iterator(content, finder);
1385                                  it != string_split_iterator();
1386                                  ++it) {
1387                                 total++;
1388                             }
1389                             if (total != m_luids.size()) {
1390                                 SyncContext::throwError(StringPrintf("%lu items != %lu luids, must match => aborting",
1391                                                                      total, (unsigned long)m_luids.size()));
1392                             }
1393                         }
1394                         list<string>::const_iterator luidit = m_luids.begin();
1395                         for (string_split_iterator it =
1396                                  boost::make_split_iterator(content, finder);
1397                              it != string_split_iterator();
1398                              ++it) {
1399                             string luid;
1400                             if (m_update) {
1401                                 if (luidit == m_luids.end()) {
1402                                     // was checked above
1403                                     SyncContext::throwError("internal error, not enough luids");
1404                                 }
1405                                 luid = *luidit;
1406                                 ++luidit;
1407                             }
1408                             SE_LOG_SHOW(NULL, NULL, "#%d: %s",
1409                                         count,
1410                                         insertItem(raw,
1411                                                    luid,
1412                                                    string(it->begin(), it->end())).getEncoded().c_str());
1413                             count++;
1414                         }
1415                     }
1416                 } else {
1417                     ReadDir dir(m_itemPath);
1418                     int count = 0;
1419                     BOOST_FOREACH(const string &entry, dir) {
1420                         string content;
1421                         string path = m_itemPath + "/" + entry;
1422                         if (!ReadFile(path, content)) {
1423                             SyncContext::throwError(path, errno);
1424                         }
1425                         SE_LOG_SHOW(NULL, NULL, "#%d: %s: %s",
1426                                     count,
1427                                     entry.c_str(),
1428                                     insertItem(raw, "", content).getEncoded().c_str());
1429                     }
1430                 }
1431                 char *token = NULL;
1432                 err = ops.m_endDataWrite(*source, true, &token);
1433                 if (token) {
1434                     free(token);
1435                 }
1436                 CHECK_ERROR("stop writing items");
1437             } else if (m_export) {
1438                 err = ops.m_startDataRead(*source, "", "");
1439                 CHECK_ERROR("reading items");
1440
1441                 ostream *out = NULL;
1442                 cxxptr<ofstream> outFile;
1443                 if (m_itemPath == "-") {
1444                     // not actually used, falls back to SE_LOG_SHOW()
1445                     out = &std::cout;
1446                 } else if(!isDir(m_itemPath)) {
1447                     outFile.set(new ofstream(m_itemPath.c_str()));
1448                     out = outFile;
1449                 }
1450                 if (m_luids.empty()) {
1451                     readLUIDs(source, m_luids);
1452                 }
1453                 bool haveItem = false;     // have written one item
1454                 bool haveNewline = false;  // that item had a newline at the end
1455                 BOOST_FOREACH(const string &luid, m_luids) {
1456                     string item;
1457                     raw->readItemRaw(luid, item);
1458                     if (!out) {
1459                         // write into directory
1460                         string fullPath = m_itemPath + "/" + luid;
1461                         ofstream file((m_itemPath + "/" + luid).c_str());
1462                         file << item;
1463                         file.close();
1464                         if (file.bad()) {
1465                             SyncContext::throwError(fullPath, errno);
1466                         }
1467                     } else {
1468                         std::string delimiter;
1469                         if (haveItem) {
1470                             if (m_delimiter.size() > 1 &&
1471                                 haveNewline &&
1472                                 m_delimiter[0] == '\n') {
1473                                 // already wrote initial newline, skip it
1474                                 delimiter = m_delimiter.substr(1);
1475                             } else {
1476                                 delimiter = m_delimiter;
1477                             }
1478                         }
1479                         if (out == &std::cout) {
1480                             // special case, use logging infrastructure
1481                             SE_LOG_SHOW(NULL, NULL, "%s%s",
1482                                         delimiter.c_str(),
1483                                         item.c_str());
1484                             // always prints newline
1485                             haveNewline = true;
1486                         } else {
1487                             // write to file
1488                             *out << item;
1489                             haveNewline = boost::ends_with(item, "\n");
1490                         }
1491                         haveItem = true;
1492                     }
1493                 }
1494                 if (outFile) {
1495                     outFile->close();
1496                     if (outFile->bad()) {
1497                         SyncContext::throwError(m_itemPath, errno);
1498                     }
1499                 }
1500             }
1501         }
1502         source->close();
1503     } else {
1504         if (!needConfigName()) {
1505             return false;
1506         }
1507
1508         std::set<std::string> unmatchedSources;
1509         boost::shared_ptr<SyncContext> context;
1510         context.reset(createSyncClient());
1511         context->setConfigProps(m_props);
1512         context->setQuiet(m_quiet);
1513         context->setDryRun(m_dryrun);
1514         context->setConfigFilter(true, "", m_props.createSyncFilter(m_server));
1515         if (m_sources.empty()) {
1516             // Special semantic of 'no source selected': apply
1517             // filter (if any exists) only to sources which are
1518             // *active*. Configuration of inactive sources is left
1519             // unchanged. This way we don't activate sync sources
1520             // accidentally when the sync mode is modified
1521             // temporarily.
1522             BOOST_FOREACH(const std::string &source,
1523                           context->getSyncSources()) {
1524                 boost::shared_ptr<PersistentSyncSourceConfig> source_config =
1525                     context->getSyncSourceConfig(source);
1526                 if (!source_config->isDisabled()) {
1527                     context->setConfigFilter(false, source, m_props.createSourceFilter(m_server, source));
1528                 }
1529             }
1530         } else {
1531             // apply (possibly empty) source filter to selected sources
1532             BOOST_FOREACH(const std::string &source,
1533                           m_sources) {
1534                 boost::shared_ptr<PersistentSyncSourceConfig> source_config =
1535                         context->getSyncSourceConfig(source);
1536                 ConfigProps filter = m_props.createSourceFilter(m_server, source);
1537                 if (!source_config || !source_config->exists()) {
1538                     // invalid source name in m_sources, remember and
1539                     // report this below
1540                     unmatchedSources.insert(source);
1541                 } else if (filter.find("sync") == filter.end()) {
1542                     // Sync mode is not set, must override the
1543                     // "sync=disabled" set below with the original
1544                     // sync mode for the source or (if that is also
1545                     // "disabled") with "two-way". The latter is part
1546                     // of the command line semantic that listing a
1547                     // source activates it.
1548                     string sync = source_config->getSync();
1549                     filter["sync"] =
1550                         sync == "disabled" ? "two-way" : sync;
1551                     context->setConfigFilter(false, source, filter);
1552                 } else {
1553                     // sync mode is set, can use m_sourceProps
1554                     // directly to apply it
1555                     context->setConfigFilter(false, source, filter);
1556                 }
1557             }
1558
1559             // temporarily disable the rest
1560             FilterConfigNode::ConfigFilter disabled;
1561             disabled["sync"] = InitStateString("disabled", true);
1562             context->setConfigFilter(false, "", disabled);
1563         }
1564
1565         // check whether there were any sources specified which do not exist
1566         if (unmatchedSources.size()) {
1567             context->throwError(string("no such source(s): ") + boost::join(unmatchedSources, " "));
1568         }
1569
1570         if (m_status) {
1571             context->status();
1572         } else if (m_printSessions) {
1573             vector<string> dirs;
1574             context->getSessions(dirs);
1575             bool first = true;
1576             BOOST_FOREACH(const string &dir, dirs) {
1577                 if (first) {
1578                     first = false;
1579                 } else if(!m_quiet) {
1580                     SE_LOG_SHOW(NULL, NULL, "\n");
1581                 }
1582                 SE_LOG_SHOW(NULL, NULL, "%s", dir.c_str());
1583                 if (!m_quiet) {
1584                     SyncReport report;
1585                     context->readSessionInfo(dir, report);
1586                     ostringstream out;
1587                     out << report;
1588                     SE_LOG_SHOW(NULL, NULL, "%s", out.str().c_str());
1589                 }
1590             }
1591         } else if (!m_restore.empty()) {
1592             // sanity checks: either --after or --before must be given, sources must be selected
1593             if ((!m_after && !m_before) ||
1594                 (m_after && m_before)) {
1595                 usage(false, "--restore <log dir> must be used with either --after (restore database as it was after that sync) or --before (restore data from before sync)");
1596                 return false;
1597             }
1598             if (m_sources.empty()) {
1599                 usage(false, "Sources must be selected explicitly for --restore to prevent accidental restore.");
1600                 return false;
1601             }
1602             context->restore(m_restore,
1603                              m_after ?
1604                              SyncContext::DATABASE_AFTER_SYNC :
1605                              SyncContext::DATABASE_BEFORE_SYNC);
1606         } else {
1607             if (m_dryrun) {
1608                 usage(false, "--dry-run not supported for running a synchronization");
1609                 return false;
1610             }
1611
1612             // safety catch: if props are given, then --run
1613             // is required
1614             if (!m_run &&
1615                 (m_props.hasProperties(FullProps::IGNORE_GLOBAL_PROPS))) {
1616                 usage(false, "Properties specified, but neither '--configure' nor '--run' - what did you want?");
1617                 return false;
1618             }
1619
1620             return (context->sync(&m_report) == STATUS_OK);
1621         }
1622     }
1623
1624     return true;
1625 }
1626
1627 void Cmdline::readLUIDs(SyncSource *source, list<string> &luids)
1628 {
1629     const SyncSource::Operations &ops = source->getOperations();
1630     sysync::ItemIDType id;
1631     sysync::sInt32 status;
1632     sysync::TSyError err = ops.m_readNextItem(*source, &id, &status, true);
1633     CHECK_ERROR("next item");
1634     while (status != sysync::ReadNextItem_EOF) {
1635         luids.push_back(id.item);
1636         StrDispose(id.item);
1637         StrDispose(id.parent);
1638         err = ops.m_readNextItem(*source, &id, &status, false);
1639         CHECK_ERROR("next item");
1640     }
1641 }
1642
1643 CmdlineLUID Cmdline::insertItem(SyncSourceRaw *source, const string &luid, const string &data)
1644 {
1645     SyncSourceRaw::InsertItemResult res = source->insertItemRaw(luid, data);
1646     CmdlineLUID cluid;
1647     cluid.setLUID(res.m_luid);
1648     return cluid;
1649 }
1650
1651 string Cmdline::cmdOpt(const char *opt, const char *param)
1652 {
1653     string res = "'";
1654     if (opt) {
1655         res += opt;
1656     }
1657     // parameter was provided as part of option
1658     bool included = opt && param &&
1659         boost::ends_with(std::string(opt),
1660                          std::string("=") + param);
1661     if (!included && opt && param) {
1662         res += " ";
1663     }
1664     if (!included && param) {
1665         res += param;
1666     }
1667     res += "'";
1668     return res;
1669 }
1670
1671 bool Cmdline::parseProp(PropertyType propertyType,
1672                         const char *opt,
1673                         const char *param,
1674                         const char *propname)
1675 {
1676     std::string args = cmdOpt(opt, param);
1677
1678     if (!param) {
1679         usage(false, string("missing parameter for ") + args);
1680         return false;
1681     }
1682
1683     // determine property name and parameter for it
1684     string propstr;
1685     string paramstr;
1686     if (propname) {
1687         propstr = propname;
1688         paramstr = param;
1689     } else if (boost::trim_copy(string(param)) == "?") {
1690         paramstr = param;
1691     } else {
1692         const char *equal = strchr(param, '=');
1693         if (!equal) {
1694             usage(false, string("the '=<value>' part is missing in: ") + args);
1695             return false;
1696         }
1697         propstr.assign(param, equal - param);
1698         paramstr.assign(equal + 1);
1699     }
1700     boost::trim(propstr);
1701     boost::trim_left(paramstr);
1702
1703     // parse full property string
1704     PropertySpecifier spec = PropertySpecifier::StringToPropSpec(propstr);
1705
1706     // determine property type and registry
1707     const ConfigPropertyRegistry *validProps = NULL;
1708     switch (propertyType) {
1709     case SYNC_PROPERTY_TYPE:
1710         validProps = &m_validSyncProps;
1711         break;
1712     case SOURCE_PROPERTY_TYPE:
1713         validProps = &m_validSourceProps;
1714         break;
1715     case UNKNOWN_PROPERTY_TYPE:
1716         // must guess based on both registries
1717         if (!propstr.empty()) {
1718             bool isSyncProp = m_validSyncProps.find(spec.m_property) != NULL;
1719             bool isSourceProp = m_validSourceProps.find(spec.m_property) != NULL;
1720
1721             if (isSyncProp) {
1722                 if (isSourceProp) {
1723                     usage(false, StringPrintf("property '%s' in %s could be both a sync and a source property, use --sync-property or --source-property to disambiguate it", propname, args.c_str()));
1724                     return false;
1725                 } else {
1726                     validProps = &m_validSyncProps;
1727                 }
1728             } else if (isSourceProp ||
1729                        boost::iequals(spec.m_property, "type")) {
1730                 validProps = &m_validSourceProps;
1731             } else {
1732                 if (propname) {
1733                     usage(false, StringPrintf("unrecognized property '%s' in %s", propname, args.c_str()));
1734                 } else {
1735                     usage(false, StringPrintf("unrecognized property in %s", args.c_str()));
1736                 }
1737                 return false;
1738             }
1739         } else {
1740             usage(false, StringPrintf("a property name must be given in %s", args.c_str()));
1741             return false;
1742         }
1743     }
1744
1745     if (boost::trim_copy(string(param)) == "?") {
1746         m_dontrun = true;
1747         if (propname) {
1748             return listPropValues(*validProps, spec.m_property, opt ? opt : "");
1749         } else {
1750             return listProperties(*validProps, opt ? opt : "");
1751         }
1752     } else {
1753         if (boost::trim_copy(paramstr) == "?") {
1754             m_dontrun = true;
1755             return listPropValues(*validProps, spec.m_property, args);
1756         } else {
1757             const ConfigProperty *prop = validProps->find(spec.m_property);
1758             if (!prop && boost::iequals(spec.m_property, "type")) {
1759                 // compatiblity mode for "type": map to the properties which
1760                 // replaced it
1761                 prop = validProps->find("backend");
1762                 if (!prop) {
1763                     SE_LOG_ERROR(NULL, NULL, "backend: no such property");
1764                     return false;
1765                 }
1766                 SourceType sourceType(paramstr);
1767                 string error;
1768                 if (!prop->checkValue(sourceType.m_backend, error)) {
1769                     SE_LOG_ERROR(NULL, NULL, "%s: %s", args.c_str(), error.c_str());
1770                     return false;
1771                 }
1772                 ContextProps &props = m_props[spec.m_config];
1773                 props.m_sourceProps[spec.m_source]["backend"] =
1774                     InitStateString(sourceType.m_backend,
1775                                     !sourceType.m_backend.empty());
1776                 props.m_sourceProps[spec.m_source]["databaseFormat"] =
1777                     InitStateString(sourceType.m_localFormat,
1778                                     !sourceType.m_localFormat.empty());
1779                 props.m_sourceProps[spec.m_source]["syncFormat"] =
1780                     InitStateString(sourceType.m_format,
1781                                     !sourceType.m_format.empty());
1782                 props.m_sourceProps[spec.m_source]["forceSyncFormat"] =
1783                     sourceType.m_forceFormat ?
1784                     InitStateString("1", true) :
1785                     InitStateString("0", false);
1786                 return true;
1787             } else if (!prop) {
1788                 SE_LOG_ERROR(NULL, NULL, "%s: no such property", args.c_str());
1789                 return false;
1790             } else {
1791                 string error;
1792                 if (!prop->checkValue(paramstr, error)) {
1793                     SE_LOG_ERROR(NULL, NULL, "%s: %s", args.c_str(), error.c_str());
1794                     return false;
1795                 } else {
1796                     ContextProps &props = m_props[spec.m_config];
1797                     if (validProps == &m_validSyncProps) {
1798                         // complain if sync property includes source prefix
1799                         if (!spec.m_source.empty()) {
1800                             SE_LOG_ERROR(NULL, NULL, "%s: source name '%s' not allowed in sync property",
1801                                          args.c_str(),
1802                                          spec.m_source.c_str());
1803                             return false;
1804                         }
1805                         props.m_syncProps[spec.m_property] = paramstr;
1806                     } else {
1807                         props.m_sourceProps[spec.m_source][spec.m_property] = paramstr;
1808                     }
1809                     return true;                        
1810                 }
1811             }
1812         }
1813     }
1814 }
1815
1816 bool Cmdline::parseAssignment(int &opt, vector<string> &parsed,
1817                               PropertyType propertyType,
1818                               const char *propname,
1819                               const char *def)
1820 {
1821     string param;
1822     bool haveParam = false;
1823     string cmdopt(m_argv[opt]);
1824     size_t off = cmdopt.find('=');
1825     if (off != cmdopt.npos) {
1826         // value embedded in option
1827         param = cmdopt.substr(off + 1);
1828         haveParam = true;
1829     } else if (!def && ++opt < m_argc) {
1830         // assume next entry is parameter
1831         param = m_argv[opt];
1832         parsed.push_back(m_argv[opt]);
1833         haveParam = true;
1834     } else if (def) {
1835         // use default
1836         param = def;
1837         haveParam = true;
1838     }
1839
1840     return parseProp(propertyType,
1841                      cmdopt.c_str(),
1842                      haveParam ? param.c_str() : NULL,
1843                      propname);
1844 }
1845
1846 bool Cmdline::listPropValues(const ConfigPropertyRegistry &validProps,
1847                                           const string &propName,
1848                                           const string &opt)
1849 {
1850     const ConfigProperty *prop = validProps.find(propName);
1851     if (!prop && boost::iequals(propName, "type")) {
1852         SE_LOG_SHOW(NULL, NULL,
1853                     "%s\n"
1854                     "   <backend>[:<format>[:<version][!]]\n"
1855                     "   legacy property, replaced by 'backend', 'databaseFormat',\n"
1856                     "   'syncFormat', 'forceSyncFormat'",
1857                     opt.c_str());
1858         return true;
1859     } else if (!prop) {
1860         SE_LOG_ERROR(NULL, NULL, "%s: no such property", opt.c_str());
1861         return false;
1862     } else {
1863         ostringstream out;
1864         out << opt << endl;
1865         string comment = prop->getComment();
1866
1867         if (comment != "") {
1868             list<string> commentLines;
1869             ConfigProperty::splitComment(comment, commentLines);
1870             BOOST_FOREACH(const string &line, commentLines) {
1871                 out << "   " << line << endl;
1872             }
1873         } else {
1874             out << "   no documentation available" << endl;
1875         }
1876         SE_LOG_SHOW(NULL, NULL, "%s", out.str().c_str());
1877         return true;
1878     }
1879 }
1880
1881 bool Cmdline::listProperties(const ConfigPropertyRegistry &validProps,
1882                                           const string &opt)
1883 {
1884     // The first of several related properties has a comment.
1885     // Remember that comment and print it as late as possible,
1886     // that way related properties preceed their comment.
1887     string comment;
1888     bool needComma = false;
1889     ostringstream out;
1890     BOOST_FOREACH(const ConfigProperty *prop, validProps) {
1891         if (!prop->isHidden()) {
1892             string newComment = prop->getComment();
1893
1894             if (newComment != "") {
1895                 if (!comment.empty()) {
1896                     out << endl;
1897                     dumpComment(out, "   ", comment);
1898                     out << endl;
1899                     needComma = false;
1900                 }
1901                 comment = newComment;
1902             }
1903             std::string def = prop->getDefValue();
1904             if (def.empty()) {
1905                 def = "no default";
1906             }
1907             ConfigProperty::Sharing sharing = prop->getSharing();
1908             if (needComma) {
1909                 out << ", ";
1910             }
1911             out << boost::join(prop->getNames(), " = ")
1912                   << " (" << def << ", "
1913                   << ConfigProperty::sharing2str(sharing)
1914                   << (prop->isObligatory() ? ", required" : "")
1915                   << ")";
1916             needComma = true;
1917         }
1918     }
1919     out << endl;
1920     dumpComment(out, "   ", comment);
1921     SE_LOG_SHOW(NULL, NULL, "%s", out.str().c_str());
1922     return true;
1923 }
1924
1925 static void findPeerProps(FilterConfigNode::ConfigFilter &filter,
1926                           ConfigPropertyRegistry &registry,
1927                           set<string> &peerProps)
1928 {
1929     BOOST_FOREACH(StringPair entry, filter) {
1930         const ConfigProperty *prop = registry.find(entry.first);
1931         if (prop &&
1932             prop->getSharing() == ConfigProperty::NO_SHARING) {
1933             peerProps.insert(entry.first);
1934         }
1935     }
1936 }
1937
1938 void Cmdline::checkForPeerProps()
1939 {
1940     set<string> peerProps;
1941
1942     BOOST_FOREACH(FullProps::value_type &entry, m_props) {
1943         ContextProps &props = entry.second;
1944
1945         findPeerProps(props.m_syncProps, SyncConfig::getRegistry(), peerProps);
1946         BOOST_FOREACH(SourceProps::value_type &entry, props.m_sourceProps) {
1947             findPeerProps(entry.second, SyncSourceConfig::getRegistry(), peerProps);
1948         }
1949     }
1950     if (!peerProps.empty()) {
1951         string props = boost::join(peerProps, ", ");
1952         if (props == "forceSyncFormat, syncFormat") {
1953             // special case: these two properties might have been added by the
1954             // legacy "sync" property, which applies to both shared and unshared
1955             // properties => cannot determine that here anymore, so ignore it
1956         } else {
1957             SyncContext::throwError(string("per-peer (unshared) properties not allowed: ") +
1958                                     props);
1959         }
1960     }
1961 }
1962
1963 void Cmdline::listSources(SyncSource &syncSource, const string &header)
1964 {
1965     ostringstream out;
1966     out << header << ":\n";
1967
1968     if (syncSource.isInactive()) {
1969         out << "not enabled during compilation or not usable in the current environment\n";
1970     } else {
1971         SyncSource::Databases databases = syncSource.getDatabases();
1972
1973         BOOST_FOREACH(const SyncSource::Database &database, databases) {
1974             out << "   " << database.m_name << " (" << database.m_uri << ")";
1975             if (database.m_isDefault) {
1976                 out << " <default>";
1977             }
1978             out << endl;
1979         }
1980     }
1981     SE_LOG_SHOW(NULL, NULL, "%s", out.str().c_str());
1982 }
1983
1984 void Cmdline::dumpConfigs(const string &preamble,
1985                                        const SyncConfig::ConfigList &servers)
1986 {
1987     ostringstream out;
1988     out << preamble << endl;
1989     BOOST_FOREACH(const SyncConfig::ConfigList::value_type &server,servers) {
1990         out << "   "  << server.first << " = " << server.second <<endl;
1991     }
1992     if (!servers.size()) {
1993         out << "   none" << endl;
1994     }
1995     SE_LOG_SHOW(NULL, NULL, "%s", out.str().c_str());
1996 }
1997
1998 void Cmdline::dumpConfigTemplates(const string &preamble,
1999                                   const SyncConfig::TemplateList &templates,
2000                                   bool printRank,
2001                                   Logger::Level level)
2002 {
2003     ostringstream out;
2004     out << preamble << endl;
2005     out << "   "  << "template name" << " = " << "template description";
2006     if (printRank) {
2007         out << "    " << "matching score in percent (100% = exact match)";
2008     }
2009     out << endl;
2010
2011     BOOST_FOREACH(const SyncConfig::TemplateList::value_type server,templates) {
2012         out << "   "  << server->m_templateId << " = " << server->m_description;
2013         if (printRank){
2014             out << "    " << server->m_rank *20 << "%";
2015         }
2016         out << endl;
2017     }
2018     if (!templates.size()) {
2019         out << "   none" << endl;
2020     }
2021     SE_LOG(level, NULL, NULL, "%s", out.str().c_str());
2022 }
2023
2024 void Cmdline::dumpProperties(const ConfigNode &configuredProps,
2025                              const ConfigPropertyRegistry &allProps,
2026                              int flags)
2027 {
2028     list<string> perPeer, perContext, global;
2029     ostringstream out;
2030
2031     BOOST_FOREACH(const ConfigProperty *prop, allProps) {
2032         if (prop->isHidden() ||
2033             ((flags & HIDE_PER_PEER) &&
2034              prop->getSharing() == ConfigProperty::NO_SHARING)) {
2035             continue;
2036         }
2037         if (!m_quiet) {
2038             string comment = prop->getComment();
2039             if (!comment.empty()) {
2040                 out << endl;
2041                 dumpComment(out, "# ", comment);
2042             }
2043         }
2044         InitStateString value = prop->getProperty(configuredProps);
2045         if (!value.wasSet()) {
2046             out << "# ";
2047         }
2048         out << prop->getMainName() << " = " << value.get() << endl;
2049
2050         list<string> *type = NULL;
2051         switch (prop->getSharing()) {
2052         case ConfigProperty::GLOBAL_SHARING:
2053             type = &global;
2054             break;
2055         case ConfigProperty::SOURCE_SET_SHARING:
2056             type = &perContext;
2057             break;
2058         case ConfigProperty::NO_SHARING:
2059             type = &perPeer;
2060             break;
2061         }
2062         if (type) {
2063             type->push_back(prop->getMainName());
2064         }
2065     }
2066
2067     if (!m_quiet && !(flags & HIDE_LEGEND)) {
2068         if (!perPeer.empty() ||
2069             !perContext.empty() ||
2070             !global.empty()) {
2071             out << endl;
2072         }
2073         if (!perPeer.empty()) {
2074             out << "# per-peer (unshared) properties: " << boost::join(perPeer, ", ") << endl;
2075         }
2076         if (!perContext.empty()) {
2077             out << "# shared by peers in same context: " << boost::join(perContext, ", ") << endl;
2078         }
2079         if (!global.empty()) {
2080             out << "# global properties: " << boost::join(global, ", ") << endl;
2081         }
2082     }
2083
2084     SE_LOG_SHOW(NULL, NULL, "%s", out.str().c_str());
2085 }
2086
2087 void Cmdline::dumpComment(ostream &stream,
2088                                        const string &prefix,
2089                                        const string &comment)
2090 {
2091     list<string> commentLines;
2092     ConfigProperty::splitComment(comment, commentLines);
2093     BOOST_FOREACH(const string &line, commentLines) {
2094         stream << prefix << line << endl;
2095     }
2096 }
2097
2098 void Cmdline::usage(bool full, const string &error, const string &param)
2099 {
2100     SE_LOG_SHOW(NULL, NULL, "%s", synopsis);
2101     if (full) {
2102         SE_LOG_SHOW(NULL, NULL, "\nOptions:\n%s", options);
2103     }
2104
2105     if (error != "") {
2106         SE_LOG_SHOW(NULL, NULL, "\n");
2107         SE_LOG_ERROR(NULL, NULL, "%s", error.c_str());
2108     }
2109     if (param != "") {
2110         SE_LOG_INFO(NULL, NULL, "use '%s%s?' to get a list of valid parameters",
2111                     param.c_str(),
2112                     boost::ends_with(param, "=") ? "" : " ");
2113     }
2114 }
2115
2116 bool Cmdline::needConfigName()
2117 {
2118     if (m_server.empty()) {
2119         usage(false, "No configuration name specified.");
2120         return false;
2121     } else {
2122         return true;
2123     }
2124 }
2125
2126
2127 SyncContext* Cmdline::createSyncClient() {
2128     return new SyncContext(m_server, true);
2129 }
2130
2131 #ifdef ENABLE_UNIT_TESTS
2132
2133 /** simple line-by-line diff */
2134 static string diffStrings(const string &lhs, const string &rhs)
2135 {
2136     ostringstream res;
2137
2138     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
2139     string_split_iterator lit =
2140         boost::make_split_iterator(lhs, boost::first_finder("\n", boost::is_iequal()));
2141     string_split_iterator rit =
2142         boost::make_split_iterator(rhs, boost::first_finder("\n", boost::is_iequal()));
2143     while (lit != string_split_iterator() &&
2144            rit != string_split_iterator()) {
2145         if (*lit != *rit) {
2146             res << "< " << *lit << endl;
2147             res << "> " << *rit << endl;
2148         }
2149         ++lit;
2150         ++rit;
2151     }
2152
2153     while (lit != string_split_iterator()) {
2154         res << "< " << *lit << endl;
2155         ++lit;
2156     }
2157
2158     while (rit != string_split_iterator()) {
2159         res << "> " << *rit << endl;
2160         ++rit;
2161     }
2162
2163     return res.str();
2164 }
2165
2166 # define CPPUNIT_ASSERT_EQUAL_DIFF( expected, actual )      \
2167     do { \
2168         string expected_ = (expected);                                  \
2169         string actual_ = (actual);                                      \
2170         if (expected_ != actual_) {                                     \
2171             CPPUNIT_NS::Message cpputMsg_(string("expected:\n") +       \
2172                                           expected_);                   \
2173             cpputMsg_.addDetail(string("actual:\n") +                   \
2174                                 actual_);                               \
2175             cpputMsg_.addDetail(string("diff:\n") +                     \
2176                                 diffStrings(expected_, actual_));       \
2177             CPPUNIT_NS::Asserter::fail( cpputMsg_,                      \
2178                                         CPPUNIT_SOURCELINE() );         \
2179         } \
2180     } while ( false )
2181
2182 // true if <word> =
2183 static bool isPropAssignment(const string &buffer) {
2184     // ignore these comments (occur in type description)
2185     if (boost::starts_with(buffer, "KCalExtended = ") ||
2186         boost::starts_with(buffer, "mkcal = ") ||
2187         boost::starts_with(buffer, "QtContacts = ")) {
2188         return false;
2189     }
2190                                 
2191     size_t start = 0;
2192     while (start < buffer.size() &&
2193            !isspace(buffer[start])) {
2194         start++;
2195     }
2196     if (start + 3 <= buffer.size() &&
2197         buffer.substr(start, 3) == " = ") {
2198         return true;
2199     } else {
2200         return false;
2201     }
2202 }
2203
2204 // remove pure comment lines from buffer,
2205 // also empty lines,
2206 // also defaultPeer and keyring (because reference properties do not include global props)
2207 static string filterConfig(const string &buffer)
2208 {
2209     ostringstream res;
2210
2211     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
2212     for (string_split_iterator it =
2213              boost::make_split_iterator(buffer, boost::first_finder("\n", boost::is_iequal()));
2214          it != string_split_iterator();
2215          ++it) {
2216         string line = boost::copy_range<string>(*it);
2217         if (!line.empty() &&
2218             line.find("defaultPeer =") == line.npos &&
2219             line.find("keyring =") == line.npos &&
2220             (!boost::starts_with(line, "# ") ||
2221              isPropAssignment(line.substr(2)))) {
2222             res << line << endl;
2223         }
2224     }
2225
2226     return res.str();
2227 }
2228
2229 static string removeComments(const string &buffer)
2230 {
2231     ostringstream res;
2232
2233     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
2234     for (string_split_iterator it =
2235              boost::make_split_iterator(buffer, boost::first_finder("\n", boost::is_iequal()));
2236          it != string_split_iterator();
2237          ++it) {
2238         string line = boost::copy_range<string>(*it);
2239         if (!line.empty() &&
2240             !boost::starts_with(line, "#")) {
2241             res << line << endl;
2242         }
2243     }
2244
2245     return res.str();
2246 }
2247
2248 // remove comment lines from scanFiles() output
2249 static string filterFiles(const string &buffer)
2250 {
2251     ostringstream res;
2252
2253     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
2254     for (string_split_iterator it =
2255              boost::make_split_iterator(buffer, boost::first_finder("\n", boost::is_iequal()));
2256          it != string_split_iterator();
2257          ++it) {
2258         string line = boost::copy_range<string>(*it);
2259         if (line.find(":#") == line.npos) {
2260             res << line;
2261             // do not add extra newline after last newline
2262             if (!line.empty() || it->end() < buffer.end()) {
2263                 res << endl;
2264             }
2265         }
2266     }
2267
2268     return res.str();
2269 }
2270
2271
2272 static string injectValues(const string &buffer)
2273 {
2274     string res = buffer;
2275
2276 #if 0
2277     // username/password not set in templates, only in configs created
2278     // via the command line - not anymore, but if it ever comes back,
2279     // here's the place for it
2280     boost::replace_first(res,
2281                          "# username = ",
2282                          "username = your SyncML server account name");
2283     boost::replace_first(res,
2284                          "# password = ",
2285                          "password = your SyncML server password");
2286 #endif
2287     return res;
2288 }
2289
2290 // remove lines indented with spaces
2291 static string filterIndented(const string &buffer)
2292 {
2293     ostringstream res;
2294     bool first = true;
2295
2296     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
2297     for (string_split_iterator it =
2298              boost::make_split_iterator(buffer, boost::first_finder("\n", boost::is_iequal()));
2299          it != string_split_iterator();
2300          ++it) {
2301         if (!boost::starts_with(*it, " ")) {
2302             if (!first) {
2303                 res << endl;
2304             } else {
2305                 first = false;
2306             }
2307             res << *it;
2308         }
2309     }
2310
2311     return res.str();
2312 }
2313
2314 // sort lines by file, preserving order inside each line
2315 static void sortConfig(string &config)
2316 {
2317     // file name, line number, property
2318     typedef pair<string, pair<int, string> > line_t;
2319     vector<line_t> lines;
2320     typedef boost::split_iterator<string::iterator> string_split_iterator;
2321     int linenr = 0;
2322     for (string_split_iterator it =
2323              boost::make_split_iterator(config, boost::first_finder("\n", boost::is_iequal()));
2324          it != string_split_iterator();
2325          ++it, ++linenr) {
2326         string line(it->begin(), it->end());
2327         if (line.empty()) {
2328             continue;
2329         }
2330
2331         size_t colon = line.find(':');
2332         string prefix = line.substr(0, colon);
2333         lines.push_back(make_pair(prefix, make_pair(linenr, line.substr(colon))));
2334     }
2335
2336     // stable sort because of line number
2337     sort(lines.begin(), lines.end());
2338
2339     size_t len = config.size();
2340     config.resize(0);
2341     config.reserve(len);
2342     BOOST_FOREACH(const line_t &line, lines) {
2343         config += line.first;
2344         config += line.second.second;
2345         config += "\n";
2346     }
2347 }
2348
2349 // convert the internal config dump to .ini style (--print-config)
2350 static string internalToIni(const string &config)
2351 {
2352     ostringstream res;
2353
2354     string section;
2355     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
2356     for (string_split_iterator it =
2357              boost::make_split_iterator(config, boost::first_finder("\n", boost::is_iequal()));
2358          it != string_split_iterator();
2359          ++it) {
2360         string line(it->begin(), it->end());
2361         if (line.empty()) {
2362             continue;
2363         }
2364
2365         size_t colon = line.find(':');
2366         string prefix = line.substr(0, colon);
2367
2368         // internal values are not part of the --print-config output
2369         if (boost::contains(prefix, ".internal.ini") ||
2370             boost::contains(line, "= internal value")) {
2371             continue;
2372         }
2373
2374         // --print-config also doesn't duplicate the "type" property
2375         // => remove the shared property
2376         if (boost::contains(line, ":type = ") &&
2377             boost::starts_with(line, "sources/")) {
2378             continue;
2379         }
2380
2381         // sources/<name>/config.ini or
2382         // spds/sources/<name>/config.ini
2383         size_t endslash = prefix.rfind('/');
2384         if (endslash != line.npos && endslash > 1) {
2385             size_t slash = prefix.rfind('/', endslash - 1);
2386             if (slash != line.npos) {
2387                 string newsource = prefix.substr(slash + 1, endslash - slash - 1);
2388                 if (newsource != section &&
2389                     prefix.find("/sources/") != prefix.npos &&
2390                     newsource != "syncml") {
2391                     res << "[" << newsource << "]" << endl;
2392                     section = newsource;
2393                 }
2394             }
2395         }
2396         string assignment = line.substr(colon + 1);
2397         // substitude aliases with generic values
2398         boost::replace_first(assignment, "= syncml:auth-md5", "= md5");
2399         boost::replace_first(assignment, "= syncml:auth-basix", "= basic");
2400         res << assignment << endl;
2401     }
2402
2403     return res.str();
2404 }
2405
2406 /** result of removeComments(filterRandomUUID(filterConfig())) for Google Calendar template/config */
2407 static const std::string googlecaldav =
2408                "syncURL = https://www.google.com/calendar/dav/%u/user/?SyncEvolution=Google\n"
2409                "printChanges = 0\n"
2410                "dumpData = 0\n"
2411                "deviceId = fixed-devid\n"
2412                "IconURI = image://themedimage/icons/services/google-calendar\n"
2413                "ConsumerReady = 1\n"
2414                "peerType = WebDAV\n"
2415                "[calendar]\n"
2416                "sync = two-way\n"
2417                "backend = CalDAV\n";
2418
2419 /** result of removeComments(filterRandomUUID(filterConfig())) for Yahoo Calendar + Contacts */
2420 static const std::string yahoo =
2421                "printChanges = 0\n"
2422                "dumpData = 0\n"
2423                "deviceId = fixed-devid\n"
2424                "IconURI = image://themedimage/icons/services/yahoo\n"
2425                "ConsumerReady = 1\n"
2426                "peerType = WebDAV\n"
2427                "[addressbook]\n"
2428                "sync = disabled\n"
2429                "backend = CardDAV\n"
2430                "[calendar]\n"
2431                "sync = two-way\n"
2432                "backend = CalDAV\n";
2433
2434 /**
2435  * Testing is based on a text representation of a directory
2436  * hierarchy where each line is of the format
2437  * <file path>:<line in file>
2438  *
2439  * The order of files is alphabetical, of lines in the file as
2440  * in the file. Lines in the file without line break cannot
2441  * be represented.
2442  *
2443  * The root of the hierarchy is not part of the representation
2444  * itself.
2445  */
2446 class CmdlineTest : public CppUnit::TestFixture {
2447     CPPUNIT_TEST_SUITE(CmdlineTest);
2448     CPPUNIT_TEST(testFramework);
2449     CPPUNIT_TEST(testSetupScheduleWorld);
2450     CPPUNIT_TEST(testFutureConfig);
2451     CPPUNIT_TEST(testPeerConfigMigration);
2452     CPPUNIT_TEST(testContextConfigMigration);
2453     CPPUNIT_TEST(testSetupDefault);
2454     CPPUNIT_TEST(testSetupRenamed);
2455     CPPUNIT_TEST(testSetupFunambol);
2456     CPPUNIT_TEST(testSetupSynthesis);
2457     CPPUNIT_TEST(testPrintServers);
2458     CPPUNIT_TEST(testPrintConfig);
2459     CPPUNIT_TEST(testPrintFileTemplates);
2460     CPPUNIT_TEST(testPrintFileTemplatesConfig);
2461     CPPUNIT_TEST(testTemplate);
2462     CPPUNIT_TEST(testMatchTemplate);
2463     CPPUNIT_TEST(testAddSource);
2464     CPPUNIT_TEST(testSync);
2465     CPPUNIT_TEST(testKeyring);
2466     CPPUNIT_TEST(testWebDAV);
2467     CPPUNIT_TEST(testConfigure);
2468     CPPUNIT_TEST(testConfigureTemplates);
2469     CPPUNIT_TEST(testConfigureSources);
2470     CPPUNIT_TEST(testOldConfigure);
2471     CPPUNIT_TEST(testMigrate);
2472     CPPUNIT_TEST(testMigrateContext);
2473     CPPUNIT_TEST(testMigrateAutoSync);
2474     CPPUNIT_TEST(testItemOperations);
2475     CPPUNIT_TEST_SUITE_END();
2476     
2477 public:
2478     CmdlineTest() :
2479         m_testDir("CmdlineTest")
2480     {
2481     }
2482
2483     void setUp()
2484     {
2485         rm_r(m_testDir);
2486         mkdir_p(m_testDir);
2487     }
2488
2489 protected:
2490
2491     /** verify that createFiles/scanFiles themselves work */
2492     void testFramework() {
2493         const string root(m_testDir);
2494         const string content("baz:line\n"
2495                              "caz/subdir:booh\n"
2496                              "caz/subdir2/sub:# comment\n"
2497                              "caz/subdir2/sub:# foo = bar\n"
2498                              "caz/subdir2/sub:# empty = \n"
2499                              "caz/subdir2/sub:# another comment\n"
2500                              "foo:bar1\n"
2501                              "foo:\n"
2502                              "foo: \n"
2503                              "foo:bar2\n");
2504         const string filtered("baz:line\n"
2505                               "caz/subdir:booh\n"
2506                               "caz/subdir2/sub:# foo = bar\n"
2507                               "caz/subdir2/sub:# empty = \n"
2508                               "foo:bar1\n"
2509                               "foo: \n"
2510                               "foo:bar2\n");
2511         createFiles(root, content);
2512         string res = scanFiles(root);
2513         CPPUNIT_ASSERT_EQUAL_DIFF(filtered, res);
2514     }
2515
2516     void removeRandomUUID(string &buffer) {
2517         string uuidstr = "deviceId = syncevolution-";
2518         size_t uuid = buffer.find(uuidstr);
2519         CPPUNIT_ASSERT(uuid != buffer.npos);
2520         size_t end = buffer.find("\n", uuid + uuidstr.size());
2521         CPPUNIT_ASSERT(end != buffer.npos);
2522         buffer.replace(uuid, end - uuid, "deviceId = fixed-devid");
2523     }
2524
2525     string filterRandomUUID(const string &buffer) {
2526         string copy = buffer;
2527         removeRandomUUID(copy);
2528         return copy;
2529     }
2530
2531     /** create new configurations */
2532     void testSetupScheduleWorld() { doSetupScheduleWorld(false); }
2533     void doSetupScheduleWorld(bool shared) {
2534         string root;
2535         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
2536         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2537         ScopedEnvChange home("HOME", m_testDir);
2538
2539         root = m_testDir;
2540         root += "/syncevolution/default";
2541         string peer;
2542         if (shared) {
2543             peer = root + "/peers/scheduleworld";
2544         } else {
2545             peer = root;
2546         }
2547
2548         {
2549             rm_r(peer);
2550             TestCmdline cmdline("--configure",
2551                                 "--sync-property", "proxyHost = proxy",
2552                                 "scheduleworld",
2553                                 "addressbook",
2554                                 NULL);
2555             cmdline.doit();
2556             string res = scanFiles(root);
2557             removeRandomUUID(res);
2558             string expected = ScheduleWorldConfig();
2559             sortConfig(expected);
2560             boost::replace_first(expected,
2561                                  "# proxyHost = ",
2562                                  "proxyHost = proxy");
2563             boost::replace_all(expected,
2564                                "sync = two-way",
2565                                "sync = disabled");
2566             boost::replace_first(expected,
2567                                  "addressbook/config.ini:sync = disabled",
2568                                  "addressbook/config.ini:sync = two-way");
2569             CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
2570         }
2571
2572         {
2573             rm_r(peer);
2574             TestCmdline cmdline("--configure",
2575                                 "--sync-property", "deviceID = fixed-devid",
2576                                 "scheduleworld",
2577                                 NULL);
2578             cmdline.doit();
2579             string res = scanFiles(root);
2580             string expected = ScheduleWorldConfig();
2581             sortConfig(expected);
2582             CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
2583         }
2584     }
2585
2586     void expectTooOld() {
2587         bool caught = false;
2588         try {
2589             SyncConfig config("scheduleworld");
2590         } catch (const StatusException &ex) {
2591             caught = true;
2592             if (ex.syncMLStatus() != STATUS_RELEASE_TOO_OLD) {
2593                 throw;
2594             } else {
2595                 CPPUNIT_ASSERT_EQUAL(StringPrintf("SyncEvolution %s is too old to read configuration 'scheduleworld', please upgrade SyncEvolution.", VERSION),
2596                                      string(ex.what()));
2597             }
2598         }
2599         CPPUNIT_ASSERT(caught);
2600     }
2601
2602     void testFutureConfig() {
2603         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2604         ScopedEnvChange home("HOME", m_testDir);
2605
2606         doSetupScheduleWorld(false);
2607         // bump min/cur version to something not supported, then
2608         // try to read => should fail
2609         IniFileConfigNode root(m_testDir, "/syncevolution/.internal.ini", false);
2610         IniFileConfigNode context(m_testDir + "/syncevolution/default", ".internal.ini", false);
2611         IniFileConfigNode peer(m_testDir + "/syncevolution/default/peers/scheduleworld", ".internal.ini", false);
2612         root.setProperty("rootMinVersion", StringPrintf("%d", CONFIG_ROOT_MIN_VERSION + 1));
2613         root.setProperty("rootCurVersion", StringPrintf("%d", CONFIG_ROOT_CUR_VERSION + 1));
2614         root.flush();
2615         context.setProperty("contextMinVersion", StringPrintf("%d", CONFIG_CONTEXT_MIN_VERSION + 1));
2616         context.setProperty("contextCurVersion", StringPrintf("%d", CONFIG_CONTEXT_CUR_VERSION + 1));
2617         context.flush();
2618         peer.setProperty("peerMinVersion", StringPrintf("%d", CONFIG_PEER_MIN_VERSION + 1));
2619         peer.setProperty("peerCurVersion", StringPrintf("%d", CONFIG_PEER_CUR_VERSION + 1));
2620         peer.flush();
2621
2622         expectTooOld();
2623
2624         root.setProperty("rootMinVersion", StringPrintf("%d", CONFIG_ROOT_MIN_VERSION));
2625         root.flush();
2626         expectTooOld();
2627
2628         context.setProperty("contextMinVersion", StringPrintf("%d", CONFIG_CONTEXT_MIN_VERSION));
2629         context.flush();
2630         expectTooOld();
2631
2632         // okay now
2633         peer.setProperty("peerMinVersion", StringPrintf("%d", CONFIG_PEER_MIN_VERSION));
2634         peer.flush();
2635         SyncConfig config("scheduleworld");
2636     }
2637
2638     void expectMigration(const std::string &config) {
2639         bool caught = false;
2640         try {
2641             SyncConfig c(config);
2642             c.prepareConfigForWrite();
2643         } catch (const StatusException &ex) {
2644             caught = true;
2645             if (ex.syncMLStatus() != STATUS_MIGRATION_NEEDED) {
2646                 throw;
2647             } else {
2648                 CPPUNIT_ASSERT_EQUAL(StringPrintf("Proceeding would modify config '%s' such that the "
2649                                                   "previous SyncEvolution release will not be able to use it. "
2650                                                   "Stopping now. Please explicitly acknowledge this step by "
2651                                                   "running the following command on the command line: "
2652                                                   "syncevolution --migrate '%s'",
2653                                                   config.c_str(),
2654                                                   config.c_str()),
2655                                      string(ex.what()));
2656             }
2657         }
2658         CPPUNIT_ASSERT(caught);
2659     }
2660
2661     void testPeerConfigMigration() {
2662         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2663         ScopedEnvChange home("HOME", m_testDir);
2664
2665         doSetupScheduleWorld(false);
2666         // decrease min/cur version to something no longer supported,
2667         // then try to write => should migrate in release mode and fail otherwise
2668         IniFileConfigNode peer(m_testDir + "/syncevolution/default/peers/scheduleworld", ".internal.ini", false);
2669         peer.setProperty("peerMinVersion", StringPrintf("%d", CONFIG_PEER_CUR_VERSION - 1));
2670         peer.setProperty("peerCurVersion", StringPrintf("%d", CONFIG_PEER_CUR_VERSION - 1));
2671         peer.flush();
2672
2673         SyncContext::setStableRelease(false);
2674         expectMigration("scheduleworld");
2675
2676         SyncContext::setStableRelease(true);
2677         {
2678             SyncConfig config("scheduleworld");
2679             config.prepareConfigForWrite();
2680         }
2681         {
2682             TestCmdline cmdline("--print-servers", NULL);
2683             cmdline.doit();
2684             CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
2685                                       "   scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
2686                                       "   scheduleworld.old = CmdlineTest/syncevolution/default/peers/scheduleworld.old\n",
2687                                       cmdline.m_out.str());
2688         }
2689
2690         // should be okay now
2691         SyncContext::setStableRelease(false);
2692         {
2693             SyncConfig config("scheduleworld");
2694             config.prepareConfigForWrite();
2695         }
2696
2697         // do the same migration with command line
2698         SyncContext::setStableRelease(false);
2699         rm_r(m_testDir + "/syncevolution/default/peers/scheduleworld");
2700         CPPUNIT_ASSERT_EQUAL(0, rename((m_testDir + "/syncevolution/default/peers/scheduleworld.old").c_str(),
2701                                        (m_testDir + "/syncevolution/default/peers/scheduleworld").c_str()));
2702         {
2703             TestCmdline cmdline("--migrate", "scheduleworld", NULL);
2704             cmdline.doit();
2705         }
2706         {
2707             SyncConfig config("scheduleworld");
2708             config.prepareConfigForWrite();
2709         }        
2710         {
2711             TestCmdline cmdline("--print-servers", NULL);
2712             cmdline.doit();
2713             CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
2714                                       "   scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
2715                                       "   scheduleworld.old = CmdlineTest/syncevolution/default/peers/scheduleworld.old\n",
2716                                       cmdline.m_out.str());
2717         }
2718     }
2719
2720     void testContextConfigMigration() {
2721         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2722         ScopedEnvChange home("HOME", m_testDir);
2723
2724         doSetupScheduleWorld(false);
2725         // decrease min/cur version to something no longer supported,
2726         // then try to write => should migrate in release mode and fail otherwise
2727         IniFileConfigNode context(m_testDir + "/syncevolution/default", ".internal.ini", false);
2728         context.setProperty("contextMinVersion", StringPrintf("%d", CONFIG_CONTEXT_CUR_VERSION - 1));
2729         context.setProperty("contextCurVersion", StringPrintf("%d", CONFIG_CONTEXT_CUR_VERSION - 1));
2730         context.flush();
2731
2732         SyncContext::setStableRelease(false);
2733         expectMigration("@default");
2734
2735         SyncContext::setStableRelease(true);
2736         {
2737             SyncConfig config("@default");
2738             config.prepareConfigForWrite();
2739         }
2740         {
2741             TestCmdline cmdline("--print-servers", NULL);
2742             cmdline.doit();
2743             CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
2744                                       "   scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
2745                                       "   scheduleworld.old@default.old = CmdlineTest/syncevolution/default.old/peers/scheduleworld.old\n",
2746                                       cmdline.m_out.str());
2747         }
2748
2749         // should be okay now
2750         SyncContext::setStableRelease(false);
2751         {
2752             SyncConfig config("@default");
2753             config.prepareConfigForWrite();
2754         }
2755
2756         // do the same migration with command line
2757         SyncContext::setStableRelease(false);
2758         rm_r(m_testDir + "/syncevolution/default");
2759         CPPUNIT_ASSERT_EQUAL(0, rename((m_testDir + "/syncevolution/default.old/peers/scheduleworld.old").c_str(),
2760                                        (m_testDir + "/syncevolution/default.old/peers/scheduleworld").c_str()));
2761         CPPUNIT_ASSERT_EQUAL(0, rename((m_testDir + "/syncevolution/default.old").c_str(),
2762                                        (m_testDir + "/syncevolution/default").c_str()));
2763         {
2764             TestCmdline cmdline("--migrate", "@default", NULL);
2765             cmdline.doit();
2766         }
2767         {
2768             SyncConfig config("@default");
2769             config.prepareConfigForWrite();
2770         }        
2771         {
2772             TestCmdline cmdline("--print-servers", NULL);
2773             cmdline.doit();
2774             CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
2775                                       "   scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
2776                                       "   scheduleworld.old@default.old = CmdlineTest/syncevolution/default.old/peers/scheduleworld.old\n",
2777                                       cmdline.m_out.str());
2778         }
2779     }
2780
2781
2782     void testSetupDefault() {
2783         string root;
2784         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
2785         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2786         ScopedEnvChange home("HOME", m_testDir);
2787
2788         root = m_testDir;
2789         root += "/syncevolution/default";
2790         TestCmdline cmdline("--configure",
2791                             "--template", "default",
2792                             "--sync-property", "deviceID = fixed-devid",
2793                             "some-other-server",
2794                             NULL);
2795         cmdline.doit();
2796         string res = scanFiles(root, "some-other-server");
2797         string expected = DefaultConfig();
2798         sortConfig(expected);
2799         boost::replace_all(expected, "/syncevolution/", "/some-other-server/");
2800         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
2801     }
2802
2803     void testSetupRenamed() {
2804         string root;
2805         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
2806         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2807         ScopedEnvChange home("HOME", m_testDir);
2808
2809         root = m_testDir;
2810         root += "/syncevolution/default";
2811         TestCmdline cmdline("--configure",
2812                             "--template", "scheduleworld",
2813                             "--sync-property", "deviceID = fixed-devid",
2814                             "scheduleworld2",
2815                             NULL);
2816         cmdline.doit();
2817         string res = scanFiles(root, "scheduleworld2");
2818         string expected = ScheduleWorldConfig();
2819         sortConfig(expected);
2820         boost::replace_all(expected, "/scheduleworld/", "/scheduleworld2/");
2821         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
2822     }
2823
2824     void testSetupFunambol() { doSetupFunambol(false); }
2825     void doSetupFunambol(bool shared) {
2826         string root;
2827         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
2828         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2829         ScopedEnvChange home("HOME", m_testDir);
2830
2831         root = m_testDir;
2832         root += "/syncevolution/default";
2833         string peer;
2834         if (shared) {
2835             peer = root + "/peers/funambol";
2836         } else {
2837             peer = root;
2838         }
2839
2840         rm_r(peer);
2841         const char * const argv_fixed[] = {
2842                 "--configure",
2843                 "--sync-property", "deviceID = fixed-devid",
2844                 // templates are case-insensitive
2845                 "FunamBOL",
2846                 NULL
2847         }, * const argv_shared[] = {
2848             "--configure",
2849             "FunamBOL",
2850             NULL
2851         };
2852         TestCmdline cmdline(shared ? argv_shared : argv_fixed);
2853         cmdline.doit();
2854         string res = scanFiles(root, "funambol");
2855         string expected = FunambolConfig();
2856         sortConfig(expected);
2857         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
2858     }
2859
2860     void testSetupSynthesis() { doSetupSynthesis(false); }
2861     void doSetupSynthesis(bool shared) {
2862         string root;
2863         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
2864         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2865         ScopedEnvChange home("HOME", m_testDir);
2866
2867         root = m_testDir;
2868         root += "/syncevolution/default";
2869         string peer;
2870         if (shared) {
2871             peer = root + "/peers/synthesis";
2872         } else {
2873             peer = root;
2874         }
2875         rm_r(peer);
2876         const char * const argv_fixed[] = {
2877                 "--configure",
2878                 "--sync-property", "deviceID = fixed-devid",
2879                 "synthesis",
2880                 NULL
2881         }, * const argv_shared[] = {
2882             "--configure",
2883             "synthesis",
2884             NULL
2885         };
2886         TestCmdline cmdline(shared ? argv_shared : argv_fixed);
2887         cmdline.doit();
2888         string res = scanFiles(root, "synthesis");
2889         string expected = SynthesisConfig();
2890         sortConfig(expected);
2891         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
2892     }
2893
2894     void testTemplate() {
2895         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
2896         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2897         ScopedEnvChange home("HOME", m_testDir);
2898
2899         TestCmdline failure("--template", NULL);
2900
2901         CPPUNIT_ASSERT(!failure.m_cmdline->parse());
2902         CPPUNIT_ASSERT_NO_THROW(failure.expectUsageError("[ERROR] missing parameter for '--template'\n"));
2903
2904         TestCmdline help("--template", "? ", NULL);
2905         help.doit();
2906         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates (servers):\n"
2907                                   "   template name = template description\n"
2908                                   "   eGroupware = http://www.egroupware.org\n"
2909                                   "   Funambol = http://my.funambol.com\n"
2910                                   "   Google_Calendar = event sync via CalDAV, use for the 'target-config@google-calendar' config\n"
2911                                   "   Google_Contacts = contact sync via SyncML, see http://www.google.com/support/mobile/bin/topic.py?topic=22181\n"
2912                                   "   Goosync = http://www.goosync.com/\n"
2913                                   "   Memotoo = http://www.memotoo.com\n"
2914                                   "   Mobical = https://www.everdroid.com\n"
2915                                   "   Oracle = http://www.oracle.com/technology/products/beehive/index.html\n"
2916                                   "   Ovi = http://www.ovi.com\n"
2917                                   "   ScheduleWorld = server no longer in operation\n"
2918                                   "   SyncEvolution = http://www.syncevolution.org\n"
2919                                   "   Synthesis = http://www.synthesis.ch\n"
2920                                   "   WebDAV = contact and event sync using WebDAV, use for the 'target-config@<server>' config\n"
2921                                   "   Yahoo = contact and event sync using WebDAV, use for the 'target-config@yahoo' config\n",
2922                                   help.m_out.str());
2923         CPPUNIT_ASSERT_EQUAL_DIFF("", help.m_err.str());
2924     }
2925
2926     void testMatchTemplate() {
2927         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "testcases/templates");
2928         ScopedEnvChange xdg("XDG_CONFIG_HOME", "/dev/null");
2929
2930         TestCmdline help1("--template", "?nokia 7210c", NULL);
2931         help1.doit();
2932         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates (clients):\n"
2933                 "   template name = template description    matching score in percent (100% = exact match)\n"
2934                 "   Nokia_7210c = Template for Nokia S40 series Phone    100%\n"
2935                 "   SyncEvolution_Client = SyncEvolution server side template    40%\n",
2936                 help1.m_out.str());
2937         CPPUNIT_ASSERT_EQUAL_DIFF("", help1.m_err.str());
2938         TestCmdline help2("--template", "?nokia", NULL);
2939         help2.doit();
2940         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates (clients):\n"
2941                 "   template name = template description    matching score in percent (100% = exact match)\n"
2942                 "   Nokia_7210c = Template for Nokia S40 series Phone    100%\n"
2943                 "   SyncEvolution_Client = SyncEvolution server side template    40%\n",
2944                 help2.m_out.str());
2945         CPPUNIT_ASSERT_EQUAL_DIFF("", help2.m_err.str());
2946         TestCmdline help3("--template", "?7210c", NULL);
2947         help3.doit();
2948         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates (clients):\n"
2949                 "   template name = template description    matching score in percent (100% = exact match)\n"
2950                 "   Nokia_7210c = Template for Nokia S40 series Phone    60%\n"
2951                 "   SyncEvolution_Client = SyncEvolution server side template    20%\n",
2952                 help3.m_out.str());
2953         CPPUNIT_ASSERT_EQUAL_DIFF("", help3.m_err.str());
2954         TestCmdline help4("--template", "?syncevolution client", NULL);
2955         help4.doit();
2956         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates (clients):\n"
2957                 "   template name = template description    matching score in percent (100% = exact match)\n"
2958                 "   SyncEvolution_Client = SyncEvolution server side template    100%\n"
2959                 "   Nokia_7210c = Template for Nokia S40 series Phone    40%\n",
2960                 help4.m_out.str());
2961         CPPUNIT_ASSERT_EQUAL_DIFF("", help4.m_err.str());
2962     }
2963
2964     void testPrintServers() {
2965         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
2966         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2967         ScopedEnvChange home("HOME", m_testDir);
2968
2969         doSetupScheduleWorld(false);
2970         doSetupSynthesis(true);
2971         doSetupFunambol(true);
2972
2973         TestCmdline cmdline("--print-servers", NULL);
2974         cmdline.doit();
2975         CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
2976                                   "   funambol = CmdlineTest/syncevolution/default/peers/funambol\n"
2977                                   "   scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
2978                                   "   synthesis = CmdlineTest/syncevolution/default/peers/synthesis\n",
2979                                   cmdline.m_out.str());
2980         CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2981     }
2982
2983     void testPrintConfig() {
2984         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
2985         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2986         ScopedEnvChange home("HOME", m_testDir);
2987
2988         testSetupFunambol();
2989
2990         {
2991             TestCmdline failure("--print-config", NULL);
2992             CPPUNIT_ASSERT(failure.m_cmdline->parse());
2993             CPPUNIT_ASSERT(!failure.m_cmdline->run());
2994             CPPUNIT_ASSERT_NO_THROW(failure.expectUsageError("[ERROR] --print-config requires either a --template or a server name.\n"));
2995         }
2996
2997         {
2998             TestCmdline failure("--print-config", "foo", NULL);
2999             CPPUNIT_ASSERT(failure.m_cmdline->parse());
3000             CPPUNIT_ASSERT(!failure.m_cmdline->run());
3001             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
3002             CPPUNIT_ASSERT_EQUAL(string("[ERROR] Server 'foo' has not been configured yet.\n"),
3003                                  failure.m_err.str());
3004         }
3005
3006         {
3007             TestCmdline failure("--print-config", "--template", "foo", NULL);
3008             CPPUNIT_ASSERT(failure.m_cmdline->parse());
3009             CPPUNIT_ASSERT(!failure.m_cmdline->run());
3010             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
3011             CPPUNIT_ASSERT_EQUAL(string("[ERROR] No configuration template for 'foo' available.\n"),
3012                                  failure.m_err.str());
3013         }
3014
3015         {
3016             TestCmdline cmdline("--print-config", "--template", "scheduleworld", NULL);
3017             cmdline.doit();
3018             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3019             string actual = cmdline.m_out.str();
3020             // deviceId must be the one from Funambol
3021             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
3022             string filtered = injectValues(filterConfig(actual));
3023             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(ScheduleWorldConfig())),
3024                                       filtered);
3025             // there should have been comments
3026             CPPUNIT_ASSERT(actual.size() > filtered.size());
3027         }
3028
3029         {
3030             TestCmdline cmdline("--print-config", "--template", "scheduleworld@nosuchcontext", NULL);
3031             cmdline.doit();
3032             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3033             string actual = cmdline.m_out.str();
3034             // deviceId must *not* be the one from Funambol because of the new context
3035             CPPUNIT_ASSERT(!boost::contains(actual, "deviceId = fixed-devid"));
3036         }
3037
3038         {
3039             TestCmdline cmdline("--print-config", "--template", "Default", NULL);
3040             cmdline.doit();
3041             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3042             string actual = injectValues(filterConfig(cmdline.m_out.str()));
3043             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
3044             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(DefaultConfig())),
3045                                       actual);
3046         }
3047
3048         {
3049             TestCmdline cmdline("--print-config", "funambol", NULL);
3050             cmdline.doit();
3051             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3052             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(FunambolConfig())),
3053                                       injectValues(filterConfig(cmdline.m_out.str())));
3054         }
3055
3056         {
3057             // override context and template properties
3058             TestCmdline cmdline("--print-config", "--template", "scheduleworld",
3059                                 "syncURL=foo",
3060                                 "database=Personal",
3061                                 "--source-property", "sync=disabled",
3062                                 NULL);
3063             cmdline.doit();
3064             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3065             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
3066             boost::replace_first(expected,
3067                                  "syncURL = http://sync.scheduleworld.com/funambol/ds",
3068                                  "syncURL = foo");
3069             boost::replace_all(expected,
3070                                "# database = ",
3071                                "database = Personal");
3072             boost::replace_all(expected,
3073                                "sync = two-way",
3074                                "sync = disabled");
3075             string actual = injectValues(filterConfig(cmdline.m_out.str()));
3076             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
3077             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
3078                                       actual);
3079         }
3080
3081         {
3082             // override context and template properties, using legacy property name
3083             TestCmdline cmdline("--print-config", "--template", "scheduleworld",
3084                                 "--sync-property", "syncURL=foo",
3085                                 "--source-property", "evolutionsource=Personal",
3086                                 "--source-property", "sync=disabled",
3087                                 NULL);
3088             cmdline.doit();
3089             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3090             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
3091             boost::replace_first(expected,
3092                                  "syncURL = http://sync.scheduleworld.com/funambol/ds",
3093                                  "syncURL = foo");
3094             boost::replace_all(expected,
3095                                "# database = ",
3096                                "database = Personal");
3097             boost::replace_all(expected,
3098                                "sync = two-way",
3099                                "sync = disabled");
3100             string actual = injectValues(filterConfig(cmdline.m_out.str()));
3101             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
3102             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
3103                                       actual);
3104         }
3105
3106         {
3107             TestCmdline cmdline("--print-config", "--quiet",
3108                                 "--template", "scheduleworld",
3109                                 "funambol",
3110                                 NULL);
3111             cmdline.doit();
3112             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3113             string actual = cmdline.m_out.str();
3114             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
3115             CPPUNIT_ASSERT_EQUAL_DIFF(internalToIni(ScheduleWorldConfig()),
3116                                       injectValues(filterConfig(actual)));
3117         }
3118
3119         {
3120             // change shared source properties, then check template again
3121             TestCmdline cmdline("--configure",
3122                                 "--source-property", "database=Personal",
3123                                 "funambol",
3124                                 NULL);
3125             cmdline.doit();
3126             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3127         }
3128         {
3129             TestCmdline cmdline("--print-config", "--quiet",
3130                                 "--template", "scheduleworld",
3131                                 "funambol",
3132                                 NULL);
3133             cmdline.doit();
3134             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3135             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
3136             // from modified Funambol config
3137             boost::replace_all(expected,
3138                                "# database = ",
3139                                "database = Personal");
3140             string actual = injectValues(filterConfig(cmdline.m_out.str()));
3141             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
3142             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
3143                                       actual);
3144         }
3145
3146         {
3147             // print config => must not use settings from default context
3148             TestCmdline cmdline("--print-config", "--template", "scheduleworld@nosuchcontext", NULL);
3149             cmdline.doit();
3150             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3151             // source settings *not* from modified Funambol config
3152             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
3153             string actual = injectValues(filterConfig(cmdline.m_out.str()));
3154             CPPUNIT_ASSERT(!boost::contains(actual, "deviceId = fixed-devid"));
3155             removeRandomUUID(actual);
3156             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
3157                                       actual);
3158         }
3159
3160         {
3161             // create config => again, must not use settings from default context
3162             TestCmdline cmdline("--configure", "--template", "scheduleworld", "other@other", NULL);
3163             cmdline.doit();
3164             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3165         }
3166         {
3167             TestCmdline cmdline("--print-config", "other@other", NULL);
3168             cmdline.doit();
3169             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3170             // source settings *not* from modified Funambol config
3171             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
3172             string actual = injectValues(filterConfig(cmdline.m_out.str()));
3173             CPPUNIT_ASSERT(!boost::contains(actual, "deviceId = fixed-devid"));
3174             removeRandomUUID(actual);
3175             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
3176                                       actual);
3177         }
3178     }
3179
3180     void testPrintFileTemplates() {
3181         // use local copy of templates in build dir (no need to install)
3182         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "./templates");
3183         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
3184         ScopedEnvChange home("HOME", m_testDir);
3185
3186         doPrintFileTemplates();
3187     }
3188
3189     void testPrintFileTemplatesConfig() {
3190         // simulate reading templates from user's XDG HOME
3191         symlink("../templates", (m_testDir + "/syncevolution-templates").c_str());
3192         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
3193         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
3194         ScopedEnvChange home("HOME", m_testDir);
3195
3196         doPrintFileTemplates();
3197     }
3198
3199     void doPrintFileTemplates() {
3200         // Compare only the properties which are really set.
3201         //
3202         // note that "backend" will be take from the @default context if one
3203         // exists, so run this before setting up Funambol below
3204         {
3205             TestCmdline cmdline("--print-config", "--template", "google calendar", NULL);
3206             cmdline.doit();
3207             CPPUNIT_ASSERT_EQUAL_DIFF(googlecaldav,
3208                                       removeComments(filterRandomUUID(filterConfig(cmdline.m_out.str()))));
3209         }
3210
3211         {
3212             TestCmdline cmdline("--print-config", "--template", "yahoo", NULL);
3213             cmdline.doit();
3214             CPPUNIT_ASSERT_EQUAL_DIFF(yahoo,
3215                                       removeComments(filterRandomUUID(filterConfig(cmdline.m_out.str()))));
3216         }
3217
3218         testSetupFunambol();
3219
3220         {
3221             TestCmdline cmdline("--print-config", "--template", "scheduleworld", NULL);
3222             cmdline.doit();
3223             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3224             string actual = cmdline.m_out.str();
3225             // deviceId must be the one from Funambol
3226             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
3227             string filtered = injectValues(filterConfig(actual));
3228             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(ScheduleWorldConfig())),
3229                                       filtered);
3230             // there should have been comments
3231             CPPUNIT_ASSERT(actual.size() > filtered.size());
3232         }
3233
3234         {
3235             TestCmdline cmdline("--print-config", "funambol", NULL);
3236             cmdline.doit();
3237             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3238             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(FunambolConfig())),
3239                                       injectValues(filterConfig(cmdline.m_out.str())));
3240         }
3241     }
3242
3243     void testAddSource() {
3244         string root;
3245         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
3246         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
3247         ScopedEnvChange home("HOME", m_testDir);
3248
3249         testSetupScheduleWorld();
3250
3251         root = m_testDir;
3252         root += "/syncevolution/default";
3253
3254         {
3255             TestCmdline cmdline("--configure",
3256                                 "--source-property", "uri = dummy",
3257                                 "scheduleworld",
3258                                 "xyz",
3259                                 NULL);
3260             cmdline.doit();
3261             string res = scanFiles(root);
3262             string expected = ScheduleWorldConfig();
3263             expected += "\n"
3264                 "peers/scheduleworld/sources/xyz/.internal.ini:# adminData = \n"
3265                 "peers/scheduleworld/sources/xyz/.internal.ini:# synthesisID = 0\n"
3266                 "peers/scheduleworld/sources/xyz/config.ini:# sync = disabled\n"
3267                 "peers/scheduleworld/sources/xyz/config.ini:uri = dummy\n"
3268                 "peers/scheduleworld/sources/xyz/config.ini:# syncFormat = \n"
3269                 "peers/scheduleworld/sources/xyz/config.ini:# forceSyncFormat = 0\n"
3270                 "sources/xyz/config.ini:# backend = select backend\n"
3271                 "sources/xyz/config.ini:# database = \n"
3272                 "sources/xyz/config.ini:# databaseFormat = \n"
3273                 "sources/xyz/config.ini:# databaseUser = \n"
3274                 "sources/xyz/config.ini:# databasePassword = ";
3275             sortConfig(expected);
3276             CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
3277         }
3278     }
3279
3280     void testSync() {
3281         TestCmdline failure("--sync", NULL);
3282         CPPUNIT_ASSERT(!failure.m_cmdline->parse());
3283         CPPUNIT_ASSERT_NO_THROW(failure.expectUsageError("[ERROR] missing parameter for '--sync'\n"));
3284
3285         TestCmdline failure2("--sync", "foo", NULL);
3286         CPPUNIT_ASSERT(!failure2.m_cmdline->parse());
3287         CPPUNIT_ASSERT_EQUAL_DIFF("", failure2.m_out.str());
3288         CPPUNIT_ASSERT_EQUAL_DIFF("[ERROR] '--sync foo': not one of the valid values (two-way, slow, refresh-from-local, refresh-from-remote = refresh, one-way-from-local, one-way-from-remote = one-way, refresh-from-client = refresh-client, refresh-from-server = refresh-server, one-way-from-client = one-way-client, one-way-from-server = one-way-server, disabled = none)\n", failure2.m_err.str());
3289
3290         TestCmdline failure3("--sync=foo", NULL);
3291         CPPUNIT_ASSERT(!failure3.m_cmdline->parse());
3292         CPPUNIT_ASSERT_EQUAL_DIFF("", failure3.m_out.str());
3293         CPPUNIT_ASSERT_EQUAL_DIFF("[ERROR] '--sync=foo': not one of the valid values (two-way, slow, refresh-from-local, refresh-from-remote = refresh, one-way-from-local, one-way-from-remote = one-way, refresh-from-client = refresh-client, refresh-from-server = refresh-server, one-way-from-client = one-way-client, one-way-from-server = one-way-server, disabled = none)\n", failure3.m_err.str());
3294
3295         TestCmdline help("--sync", " ?", NULL);
3296         help.doit();
3297         CPPUNIT_ASSERT_EQUAL_DIFF("--sync\n"
3298                                   "   Requests a certain synchronization mode when initiating a sync:\n"
3299                                   "   \n"
3300                                   "     two-way\n"
3301                                   "       only send/receive changes since last sync\n"
3302                                   "     slow\n"
3303                                   "       exchange all items\n"
3304                                   "     refresh-from-remote\n"
3305                                   "       discard all local items and replace with\n"
3306                                   "       the items on the peer\n"
3307                                   "     refresh-from-local\n"
3308                                   "       discard all items on the peer and replace\n"
3309                                   "       with the local items\n"
3310                                   "     one-way-from-remote\n"
3311                                   "       transmit changes from peer\n"
3312                                   "     one-way-from-local\n"
3313                                   "       transmit local changes\n"
3314                                   "     disabled (or none)\n"
3315                                   "       synchronization disabled\n"
3316                                   "   \n"
3317                                   "   refresh/one-way-from-server/client are also supported. Their use is\n"
3318                                   "   discouraged because the direction of the data transfer depends\n"
3319                                   "   on the role of the local side (can be server or client), which is\n"
3320                                   "   not always obvious.\n"
3321                                   "   \n"
3322                                   "   When accepting a sync session in a SyncML server (HTTP server), only\n"
3323                                   "   sources with sync != disabled are made available to the client,\n"
3324                                   "   which chooses the final sync mode based on its own configuration.\n"
3325                                   "   When accepting a sync session in a SyncML client (local sync with\n"
3326                                   "   the server contacting SyncEvolution on a device), the sync mode\n"
3327                                   "   specified in the client is typically overriden by the server.\n",
3328                                   help.m_out.str());
3329         CPPUNIT_ASSERT_EQUAL_DIFF("", help.m_err.str());
3330
3331         TestCmdline filter("--sync", "refresh-from-server", NULL);
3332         CPPUNIT_ASSERT(filter.m_cmdline->parse());
3333         CPPUNIT_ASSERT(!filter.m_cmdline->run());
3334         CPPUNIT_ASSERT_NO_THROW(filter.expectUsageError("[ERROR] No configuration name specified.\n"));
3335         CPPUNIT_ASSERT_EQUAL_DIFF("sync = refresh-from-server",
3336                                   string(filter.m_cmdline->m_props[""].m_sourceProps[""]));
3337         CPPUNIT_ASSERT_EQUAL_DIFF("",                                  string(filter.m_cmdline->m_props[""].m_syncProps));
3338
3339         TestCmdline filter2("--source-property", "sync=refresh", NULL);
3340         CPPUNIT_ASSERT(filter2.m_cmdline->parse());
3341         CPPUNIT_ASSERT(!filter2.m_cmdline->run());
3342         CPPUNIT_ASSERT_NO_THROW(filter2.expectUsageError("[ERROR] No configuration name specified.\n"));
3343         CPPUNIT_ASSERT_EQUAL_DIFF("sync = refresh",
3344                                   string(filter2.m_cmdline->m_props[""].m_sourceProps[""]));
3345         CPPUNIT_ASSERT_EQUAL_DIFF("",
3346                                   string(filter2.m_cmdline->m_props[""].m_syncProps));
3347
3348         TestCmdline filter3("--source-property", "xyz=1", NULL);
3349         CPPUNIT_ASSERT(!filter3.m_cmdline->parse());
3350         CPPUNIT_ASSERT_EQUAL(string(""), filter3.m_out.str());
3351         CPPUNIT_ASSERT_EQUAL(string("[ERROR] '--source-property xyz=1': no such property\n"), filter3.m_err.str());
3352
3353         TestCmdline filter4("xyz=1", NULL);
3354         CPPUNIT_ASSERT(!filter4.m_cmdline->parse());
3355         CPPUNIT_ASSERT_NO_THROW(filter4.expectUsageError("[ERROR] unrecognized property in 'xyz=1'\n"));
3356
3357         TestCmdline filter5("=1", NULL);
3358         CPPUNIT_ASSERT(!filter5.m_cmdline->parse());
3359         CPPUNIT_ASSERT_NO_THROW(filter5.expectUsageError("[ERROR] a property name must be given in '=1'\n"));
3360     }
3361
3362     void testKeyring() {
3363         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
3364         ScopedEnvChange home("HOME", m_testDir);
3365
3366         rm_r(m_testDir);
3367         {
3368             TestCmdline cmdline(NULL, NULL);
3369             boost::shared_ptr<SyncContext> context = cmdline.parse();
3370             CPPUNIT_ASSERT(context);
3371             InitStateTri keyring = context->getKeyring();
3372             CPPUNIT_ASSERT_EQUAL(false, keyring.wasSet());
3373             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3374         }
3375         {
3376             TestCmdline cmdline("--keyring", NULL);
3377             boost::shared_ptr<SyncContext> context = cmdline.parse();
3378             CPPUNIT_ASSERT(context);
3379             InitStateTri keyring = context->getKeyring();
3380             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3381             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3382         }
3383         {
3384             TestCmdline cmdline("--sync-property", "keyring=True", NULL);
3385             boost::shared_ptr<SyncContext> context = cmdline.parse();
3386             CPPUNIT_ASSERT(context);
3387             InitStateTri keyring = context->getKeyring();
3388             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3389             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3390         }
3391         {
3392             TestCmdline cmdline("keyring=True", NULL);
3393             boost::shared_ptr<SyncContext> context = cmdline.parse();
3394             CPPUNIT_ASSERT(context);
3395             InitStateTri keyring = context->getKeyring();
3396             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3397             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3398         }
3399         {
3400             TestCmdline cmdline("--keyring=true", NULL);
3401             boost::shared_ptr<SyncContext> context = cmdline.parse();
3402             CPPUNIT_ASSERT(context);
3403             InitStateTri keyring = context->getKeyring();
3404             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3405             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3406         }
3407         {
3408             TestCmdline cmdline("--keyring=1", NULL);
3409             boost::shared_ptr<SyncContext> context = cmdline.parse();
3410             CPPUNIT_ASSERT(context);
3411             InitStateTri keyring = context->getKeyring();
3412             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3413             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3414         }
3415         {
3416             TestCmdline cmdline("--keyring=Yes", NULL);
3417             boost::shared_ptr<SyncContext> context = cmdline.parse();
3418             CPPUNIT_ASSERT(context);
3419             InitStateTri keyring = context->getKeyring();
3420             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3421             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3422         }
3423         {
3424             TestCmdline cmdline("--keyring=false", NULL);
3425             boost::shared_ptr<SyncContext> context = cmdline.parse();
3426             CPPUNIT_ASSERT(context);
3427             InitStateTri keyring = context->getKeyring();
3428             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3429             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_FALSE, keyring.getValue());
3430         }
3431         {
3432             TestCmdline cmdline("--keyring=0", NULL);
3433             boost::shared_ptr<SyncContext> context = cmdline.parse();
3434             CPPUNIT_ASSERT(context);
3435             InitStateTri keyring = context->getKeyring();
3436             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3437             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_FALSE, keyring.getValue());
3438         }
3439         {
3440             TestCmdline cmdline("--keyring=NO", NULL);
3441             boost::shared_ptr<SyncContext> context = cmdline.parse();
3442             CPPUNIT_ASSERT(context);
3443             InitStateTri keyring = context->getKeyring();
3444             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3445             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_FALSE, keyring.getValue());
3446         }
3447         {
3448             TestCmdline cmdline("--keyring=GNOME", NULL);
3449             boost::shared_ptr<SyncContext> context = cmdline.parse();
3450             CPPUNIT_ASSERT(context);
3451             InitStateTri keyring = context->getKeyring();
3452             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3453             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_STRING, keyring.getValue());
3454             CPPUNIT_ASSERT_EQUAL(std::string("GNOME"), keyring.get());
3455         }
3456
3457         // Broken command line: treated like a sync, but config doesn't exist.
3458         {
3459             TestCmdline cmdline("keyring=KDE", "@foobar", NULL);
3460             cmdline.doit(false);
3461             CPPUNIT_ASSERT_EQUAL(std::string(""), cmdline.m_out.str());
3462             CPPUNIT_ASSERT_EQUAL(std::string("[INFO] Configuration \"@foobar\" does not refer to a sync peer.\n[ERROR] Cannot proceed with sync without a configuration."), cmdline.m_err.str());
3463         }
3464         {
3465             TestCmdline cmdline("keyring=KDE", "nosuchpeer@foobar", NULL);
3466             cmdline.doit(false);
3467             CPPUNIT_ASSERT_EQUAL(std::string(""), cmdline.m_out.str());
3468             CPPUNIT_ASSERT_EQUAL(std::string("[INFO] Configuration \"nosuchpeer@foobar\" does not exist.\n[ERROR] Cannot proceed with sync without a configuration."), cmdline.m_err.str());
3469         }
3470
3471         // empty config prop
3472         {
3473             TestCmdline cmdline("--configure", "@default", NULL);
3474             cmdline.doit();
3475         }
3476
3477         // Try broken command line again.
3478         {
3479             TestCmdline cmdline("keyring=KDE", "@foobar", NULL);
3480             cmdline.doit(false);
3481             CPPUNIT_ASSERT_EQUAL(std::string(""), cmdline.m_out.str());
3482             CPPUNIT_ASSERT_EQUAL(std::string("[INFO] Configuration \"@foobar\" does not refer to a sync peer.\n[ERROR] Cannot proceed with sync without a configuration."), cmdline.m_err.str());
3483         }
3484         {
3485             TestCmdline cmdline("keyring=KDE", "nosuchpeer@foobar", NULL);
3486             cmdline.doit(false);
3487             CPPUNIT_ASSERT_EQUAL(std::string(""), cmdline.m_out.str());
3488             CPPUNIT_ASSERT_EQUAL(std::string("[INFO] Configuration \"nosuchpeer@foobar\" does not exist.\n[ERROR] Cannot proceed with sync without a configuration."), cmdline.m_err.str());
3489         }
3490
3491         {
3492             TestCmdline cmdline("@foobar", NULL);
3493             boost::shared_ptr<SyncContext> context = cmdline.parse();
3494             CPPUNIT_ASSERT(context);
3495             InitStateTri keyring = context->getKeyring();
3496             CPPUNIT_ASSERT_EQUAL(false, keyring.wasSet());
3497             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3498         }
3499
3500         // now set the value permanently
3501         {
3502             TestCmdline cmdline("--keyring", "--configure", "@default", NULL);
3503             cmdline.doit();
3504             boost::shared_ptr<SyncContext> context = cmdline.parse();
3505             CPPUNIT_ASSERT(context);
3506             InitStateTri keyring = context->getKeyring();
3507             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3508             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3509         }
3510         {
3511             TestCmdline cmdline("--keyring=KDE", "--configure", "@default", NULL);
3512             cmdline.doit();
3513             boost::shared_ptr<SyncContext> context = cmdline.parse();
3514             CPPUNIT_ASSERT(context);
3515             InitStateTri keyring = context->getKeyring();
3516             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3517             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_STRING, keyring.getValue());
3518             CPPUNIT_ASSERT_EQUAL(std::string("KDE"), keyring.get());
3519         }
3520
3521         // create by setting keyring in @default, then update;
3522         // @default not strictly needed
3523         rm_r(m_testDir);
3524         {
3525             TestCmdline cmdline("keyring=KDE", "--configure", "@default", NULL);
3526             cmdline.doit();
3527             boost::shared_ptr<SyncContext> context = cmdline.parse();
3528             CPPUNIT_ASSERT(context);
3529             InitStateTri keyring = context->getKeyring();
3530             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3531             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_STRING, keyring.getValue());
3532         }
3533         {
3534             TestCmdline cmdline("keyring=yes", "--configure", "@default", NULL);
3535             cmdline.doit();
3536             boost::shared_ptr<SyncContext> context = cmdline.parse();
3537             CPPUNIT_ASSERT(context);
3538             InitStateTri keyring = context->getKeyring();
3539             CPPUNIT_ASSERT_EQUAL(true, keyring.wasSet());
3540             CPPUNIT_ASSERT_EQUAL(InitStateTri::VALUE_TRUE, keyring.getValue());
3541         }
3542
3543         // allow sync operation although --keyring was set
3544         {
3545             TestCmdline cmdline("keyring=GNOME", "foobar@default", NULL);
3546             cmdline.doit(false);
3547             CPPUNIT_ASSERT_EQUAL(std::string(""), cmdline.m_out.str());
3548             CPPUNIT_ASSERT_EQUAL(std::string("[INFO] Configuration \"foobar@default\" does not exist.\n[ERROR] Cannot proceed with sync without a configuration."), cmdline.m_err.str());
3549         }
3550
3551         // catch invalid "keyring" value
3552         {
3553             TestCmdline cmdline("--configure",
3554                                 "username=foo",
3555                                 "password=bar",
3556                                 "syncURL=http://no.such.server",
3557                                 "keyring=no-such-keyring",
3558                                 "foobar@default", NULL);
3559             cmdline.doit(false);
3560             CPPUNIT_ASSERT_EQUAL(std::string(""), cmdline.m_out.str());
3561             CPPUNIT_ASSERT_EQUAL(std::string("[ERROR] Unsupported value for the \"keyring\" property, no such keyring found: no-such-keyring"),
3562                                  cmdline.m_err.str());
3563         }
3564     }
3565
3566     void testWebDAV() {
3567 #ifdef ENABLE_DAV
3568         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
3569         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
3570         ScopedEnvChange home("HOME", m_testDir);
3571
3572         // configure Yahoo under a different name, with explicit template selection
3573         {
3574             TestCmdline cmdline("--configure",
3575                                 "--template", "yahoo",
3576                                 "target-config@my-yahoo",
3577                                 NULL);
3578             cmdline.doit();
3579         }
3580         {
3581             TestCmdline cmdline("--print-config", "target-config@my-yahoo", NULL);
3582             cmdline.doit();
3583             CPPUNIT_ASSERT_EQUAL_DIFF(yahoo,
3584                                       removeComments(filterRandomUUID(filterConfig(cmdline.m_out.str()))));
3585         }
3586
3587         // configure Google Calendar with template derived from config name
3588         {
3589             TestCmdline cmdline("--configure",
3590                                 "target-config@google-calendar",
3591                                 NULL);
3592             cmdline.doit();
3593         }
3594         {
3595             TestCmdline cmdline("--print-config", "target-config@google-calendar", NULL);
3596             cmdline.doit();
3597             CPPUNIT_ASSERT_EQUAL_DIFF(googlecaldav,
3598                                       removeComments(filterRandomUUID(filterConfig(cmdline.m_out.str()))));
3599         }
3600
3601         // test "template not found" error cases
3602         {
3603             TestCmdline cmdline("--configure",
3604                                 "--template", "yahooxyz",
3605                                 "target-config@my-yahoo-xyz",
3606                                 NULL);
3607             CPPUNIT_ASSERT(cmdline.m_cmdline->parse());
3608             CPPUNIT_ASSERT(!cmdline.m_cmdline->run());
3609             static const char error[] = "[ERROR] No configuration template for 'yahooxyz' available.\n"
3610                 "[INFO] \n"
3611                 "[INFO] Available configuration templates (clients and servers):\n";
3612             std::string out = cmdline.m_out.str();
3613             std::string err = cmdline.m_err.str();
3614             std::string all = cmdline.m_all.str();
3615             CPPUNIT_ASSERT(boost::starts_with(err, error));
3616             CPPUNIT_ASSERT(boost::ends_with(err, "\n"));
3617             CPPUNIT_ASSERT(!boost::ends_with(err, "\n\n"));
3618             CPPUNIT_ASSERT_EQUAL(string(""), out);
3619             CPPUNIT_ASSERT_EQUAL(all, err);
3620         }
3621         {
3622             TestCmdline cmdline("--configure",
3623                                 "target-config@foobar",
3624                                 NULL);
3625             CPPUNIT_ASSERT(cmdline.m_cmdline->parse());
3626             CPPUNIT_ASSERT(!cmdline.m_cmdline->run());
3627             static const char error[] = "[ERROR] No configuration template for 'foobar' available.\n"
3628                 "[INFO] Use '--template none' and/or specify relevant properties on the command line to create a configuration without a template. Need values for: syncURL\n"
3629                 "[INFO] \n"
3630                 "[INFO] Available configuration templates (clients and servers):\n";
3631             std::string out = cmdline.m_out.str();
3632             std::string err = cmdline.m_err.str();
3633             std::string all = cmdline.m_all.str();
3634             CPPUNIT_ASSERT(boost::starts_with(err, error));
3635             CPPUNIT_ASSERT(boost::ends_with(err, "\n"));
3636             CPPUNIT_ASSERT(!boost::ends_with(err, "\n\n"));
3637             CPPUNIT_ASSERT_EQUAL(string(""), out);
3638             CPPUNIT_ASSERT_EQUAL(err, all);
3639         }
3640 #endif
3641     }
3642
3643     void testConfigure() {
3644         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
3645         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
3646         ScopedEnvChange home("HOME", m_testDir);
3647
3648         testSetupScheduleWorld();
3649         string expected = doConfigure(ScheduleWorldConfig(), "sources/addressbook/config.ini:");
3650
3651         {
3652             // updating "type" for peer is mapped to updating "backend",
3653             // "databaseFormat", "syncFormat", "forceSyncFormat"
3654             TestCmdline cmdline("--configure",
3655                                 "--source-property", "addressbook/type=file:text/vcard:3.0",
3656                                 "scheduleworld",
3657                                 NULL);
3658             cmdline.doit();
3659             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3660             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
3661             boost::replace_first(expected,
3662                                  "backend = addressbook",
3663                                  "backend = file");
3664             boost::replace_first(expected,
3665                                  "# databaseFormat = ",
3666                                  "databaseFormat = text/vcard");
3667             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
3668                                       filterConfig(printConfig("scheduleworld")));
3669             string shared = filterConfig(printConfig("@default"));
3670             CPPUNIT_ASSERT(shared.find("backend = file") != shared.npos);
3671             CPPUNIT_ASSERT(shared.find("databaseFormat = text/vcard") != shared.npos);
3672         }
3673
3674         {
3675             // updating type for context must not affect peer
3676             TestCmdline cmdline("--configure",
3677                                 "--source-property", "type=file:text/x-vcard:2.1",
3678                                 "@default", "addressbook",
3679                                 NULL);
3680             cmdline.doit();
3681             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3682             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
3683             boost::replace_first(expected,
3684                                  "databaseFormat = text/vcard",
3685                                  "databaseFormat = text/x-vcard");
3686             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
3687                                       filterConfig(printConfig("scheduleworld")));
3688             string shared = filterConfig(printConfig("@default"));
3689             CPPUNIT_ASSERT(shared.find("backend = file") != shared.npos);
3690             CPPUNIT_ASSERT(shared.find("databaseFormat = text/x-vcard") != shared.npos);
3691         }
3692
3693         string syncProperties("syncURL (no default, unshared, required)\n"
3694                               "\n"
3695                               "username (no default, unshared)\n"
3696                               "\n"
3697                               "password (no default, unshared)\n"
3698                               "\n"
3699                               "logdir (no default, shared)\n"
3700                               "\n"
3701                               "loglevel (0, unshared)\n"
3702                               "\n"
3703                               "notifyLevel (3, unshared)\n"
3704                               "\n"
3705                               "printChanges (TRUE, unshared)\n"
3706                               "\n"
3707                               "dumpData (TRUE, unshared)\n"
3708                               "\n"
3709                               "maxlogdirs (10, shared)\n"
3710                               "\n"
3711                               "autoSync (0, unshared)\n"
3712                               "\n"
3713                               "autoSyncInterval (30M, unshared)\n"
3714                               "\n"
3715                               "autoSyncDelay (5M, unshared)\n"
3716                               "\n"
3717                               "preventSlowSync (TRUE, unshared)\n"
3718                               "\n"
3719                               "useProxy (FALSE, unshared)\n"
3720                               "\n"
3721                               "proxyHost (no default, unshared)\n"
3722                               "\n"
3723                               "proxyUsername (no default, unshared)\n"
3724                               "\n"
3725                               "proxyPassword (no default, unshared)\n"
3726                               "\n"
3727                               "clientAuthType (md5, unshared)\n"
3728                               "\n"
3729                               "RetryDuration (5M, unshared)\n"
3730                               "\n"
3731                               "RetryInterval (2M, unshared)\n"
3732                               "\n"
3733                               "remoteIdentifier (no default, unshared)\n"
3734                               "\n"
3735                               "PeerIsClient (FALSE, unshared)\n"
3736                               "\n"
3737                               "SyncMLVersion (no default, unshared)\n"
3738                               "\n"
3739                               "PeerName (no default, unshared)\n"
3740                               "\n"
3741                               "deviceId (no default, shared)\n"
3742                               "\n"
3743                               "remoteDeviceId (no default, unshared)\n"
3744                               "\n"
3745                               "enableWBXML (TRUE, unshared)\n"
3746                               "\n"
3747                               "maxMsgSize (150000, unshared), maxObjSize (4000000, unshared)\n"
3748                               "\n"
3749                               "SSLServerCertificates (" SYNCEVOLUTION_SSL_SERVER_CERTIFICATES ", unshared)\n"
3750                               "\n"
3751                               "SSLVerifyServer (TRUE, unshared)\n"
3752                               "\n"
3753                               "SSLVerifyHost (TRUE, unshared)\n"
3754                               "\n"
3755                               "WebURL (no default, unshared)\n"
3756                               "\n"
3757                               "IconURI (no default, unshared)\n"
3758                               "\n"
3759                               "ConsumerReady (FALSE, unshared)\n"
3760                               "\n"
3761                               "peerType (no default, unshared)\n"
3762                               "\n"
3763                               "defaultPeer (no default, global)\n"
3764                               "\n"
3765                               "keyring (yes, global)\n");
3766
3767         string sourceProperties("sync (disabled, unshared, required)\n"
3768                                 "\n"
3769                                 "uri (no default, unshared)\n"
3770                                 "\n"
3771                                 "backend (select backend, shared)\n"
3772                                 "\n"
3773                                 "syncFormat (no default, unshared)\n"
3774                                 "\n"
3775                                 "forceSyncFormat (FALSE, unshared)\n"
3776                                 "\n"
3777                                 "database = evolutionsource (no default, shared)\n"
3778                                 "\n"
3779                                 "databaseFormat (no default, shared)\n"
3780                                 "\n"
3781                                 "databaseUser = evolutionuser (no default, shared), databasePassword = evolutionpassword (no default, shared)\n");
3782
3783         {
3784             TestCmdline cmdline("--sync-property", "?",
3785                                 NULL);
3786             cmdline.doit();
3787             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3788             CPPUNIT_ASSERT_EQUAL_DIFF(syncProperties,
3789                                       filterIndented(cmdline.m_out.str()));
3790         }
3791
3792         {
3793             TestCmdline cmdline("--source-property", "?",
3794                                 NULL);
3795             cmdline.doit();
3796             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3797             CPPUNIT_ASSERT_EQUAL_DIFF(sourceProperties,
3798                                       filterIndented(cmdline.m_out.str()));
3799         }
3800
3801         {
3802             TestCmdline cmdline("--source-property", "?",
3803                                 "--sync-property", "?",
3804                                 NULL);
3805             cmdline.doit();
3806             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3807             CPPUNIT_ASSERT_EQUAL_DIFF(sourceProperties + syncProperties,
3808                                       filterIndented(cmdline.m_out.str()));
3809         }
3810
3811         {
3812             TestCmdline cmdline("--sync-property", "?",
3813                                 "--source-property", "?",
3814                                 NULL);
3815             cmdline.doit();
3816             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3817             CPPUNIT_ASSERT_EQUAL_DIFF(syncProperties + sourceProperties,
3818                                       filterIndented(cmdline.m_out.str()));
3819         }
3820
3821         {
3822             TestCmdline cmdline("--source-property", "sync=?",
3823                                 NULL);
3824             cmdline.doit();
3825             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3826             CPPUNIT_ASSERT_EQUAL_DIFF("'--source-property sync=?'\n",
3827                                       filterIndented(cmdline.m_out.str()));
3828         }
3829
3830         {
3831             TestCmdline cmdline("sync=?",
3832                                 NULL);
3833             cmdline.doit();
3834             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3835             CPPUNIT_ASSERT_EQUAL_DIFF("'sync=?'\n",
3836                                       filterIndented(cmdline.m_out.str()));
3837         }
3838
3839         {
3840             TestCmdline cmdline("syncURL=?",
3841                                 NULL);
3842             cmdline.doit();
3843             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
3844             CPPUNIT_ASSERT_EQUAL_DIFF("'syncURL=?'\n",
3845                                       filterIndented(cmdline.m_out.str()));
3846         }
3847     }
3848
3849     /**
3850      * Test semantic of config creation (instead of updating) with and without
3851      * templates. See BMC #14805.
3852      */
3853     void testConfigureTemplates() {
3854         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
3855         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
3856         ScopedEnvChange home("HOME", m_testDir);
3857
3858         rm_r(m_testDir);
3859         {
3860             // catch possible typos like "sheduleworld"
3861             TestCmdline failure("--configure", "foo", NULL);
3862             CPPUNIT_ASSERT(failure.m_cmdline->parse());
3863             CPPUNIT_ASSERT(!failure.m_cmdline->run());
3864             static const char error[] = "[ERROR] No configuration template for 'foo@default' available.\n"
3865                 "[INFO] Use '--template none' and/or specify relevant properties on the command line to create a configuration without a template. Need values for: syncURL\n"
3866                 "[INFO] \n"
3867                 "[INFO] Available configuration templates (clients and servers):\n";
3868             std::string out = failure.m_out.str();
3869             std::string err = failure.m_err.str();
3870             std::string all = failure.m_all.str();
3871             CPPUNIT_ASSERT(boost::starts_with(err, error));
3872             CPPUNIT_ASSERT(boost::ends_with(err, "\n"));
3873             CPPUNIT_ASSERT(!boost::ends_with(err, "\n\n"));
3874             CPPUNIT_ASSERT_EQUAL(string(""), out);
3875             CPPUNIT_ASSERT_EQUAL(all, err);
3876         }
3877
3878         rm_r(m_testDir);
3879         {
3880             // catch possible typos like "sheduleworld" when
3881             // enough properties are specified to continue without
3882             // a template
3883             TestCmdline failure("--configure", "syncURL=http://foo.com", "--template", "foo", "bar", NULL);
3884             CPPUNIT_ASSERT(failure.m_cmdline->parse());
3885             CPPUNIT_ASSERT(!failure.m_cmdline->run());
3886
3887             static const char error[] = "[ERROR] No configuration template for 'foo' available.\n"
3888                 "[INFO] All relevant properties seem to be set, omit the --template parameter to proceed.\n"
3889                 "[INFO] \n"
3890                 "[INFO] Available configuration templates (clients and servers):\n";
3891             std::string out = failure.m_out.str();
3892             std::string err = failure.m_err.str();
3893             std::string all = failure.m_all.str();
3894             CPPUNIT_ASSERT(boost::starts_with(err, error));
3895             CPPUNIT_ASSERT(boost::ends_with(err, "\n"));
3896             CPPUNIT_ASSERT(!boost::ends_with(err, "\n\n"));
3897             CPPUNIT_ASSERT_EQUAL(string(""), out);
3898             CPPUNIT_ASSERT_EQUAL(all, err);
3899         }
3900
3901         string fooconfig =
3902             StringPrintf("syncevolution/.internal.ini:rootMinVersion = %d\n"
3903                          "syncevolution/.internal.ini:rootCurVersion = %d\n"
3904                          "syncevolution/default/.internal.ini:contextMinVersion = %d\n"
3905                          "syncevolution/default/.internal.ini:contextCurVersion = %d\n"
3906                          "syncevolution/default/config.ini:deviceId = fixed-devid\n"
3907                          "syncevolution/default/peers/foo/.internal.ini:peerMinVersion = %d\n"
3908                          "syncevolution/default/peers/foo/.internal.ini:peerCurVersion = %d\n",
3909                          CONFIG_ROOT_MIN_VERSION, CONFIG_ROOT_CUR_VERSION,
3910                          CONFIG_CONTEXT_MIN_VERSION, CONFIG_CONTEXT_CUR_VERSION,
3911                          CONFIG_PEER_MIN_VERSION, CONFIG_PEER_CUR_VERSION);
3912
3913         string syncurl =
3914             "syncevolution/default/peers/foo/config.ini:syncURL = local://@bar\n";
3915
3916         string configsource =
3917             "syncevolution/default/peers/foo/sources/eds_event/config.ini:sync = two-way\n"
3918             "syncevolution/default/sources/eds_event/config.ini:backend = calendar\n";
3919
3920         rm_r(m_testDir);
3921         {
3922             // allow user to proceed if they wish: should result in no sources configured
3923             TestCmdline failure("--configure", "--template", "none", "foo", NULL);
3924             CPPUNIT_ASSERT(failure.m_cmdline->parse());
3925             bool success  = failure.m_cmdline->run();
3926             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
3927             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_err.str());
3928             CPPUNIT_ASSERT(success);
3929             string res = scanFiles(m_testDir);
3930             removeRandomUUID(res);
3931             CPPUNIT_ASSERT_EQUAL_DIFF(fooconfig, filterFiles(res));
3932         }
3933
3934         rm_r(m_testDir);
3935         {
3936             // allow user to proceed if they wish: should result in no sources configured,
3937             // even if general source properties are specified
3938             TestCmdline failure("--configure", "--template", "none", "backend=calendar", "foo", NULL);
3939             bool success = failure.m_cmdline->parse() && failure.m_cmdline->run();
3940             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
3941             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_err.str());
3942             CPPUNIT_ASSERT(success);
3943             string res = scanFiles(m_testDir);
3944             removeRandomUUID(res);
3945             CPPUNIT_ASSERT_EQUAL_DIFF(fooconfig, filterFiles(res));
3946         }
3947
3948         rm_r(m_testDir);
3949         {
3950             // allow user to proceed if they wish: should result in no sources configured,
3951             // even if specific source properties are specified
3952             TestCmdline failure("--configure", "--template", "none", "eds_event/backend=calendar", "foo", NULL);
3953             bool success = failure.m_cmdline->parse() && failure.m_cmdline->run();
3954             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
3955             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_err.str());
3956             CPPUNIT_ASSERT(success);
3957             string res = scanFiles(m_testDir);
3958             removeRandomUUID(res);
3959             CPPUNIT_ASSERT_EQUAL_DIFF(fooconfig, filterFiles(res));
3960         }
3961
3962         rm_r(m_testDir);
3963         {
3964             // allow user to proceed if they wish and possible: here eds_event is not usable
3965             TestCmdline failure("--configure", "--template", "none", "foo", "eds_event", NULL);
3966             CPPUNIT_ASSERT(failure.m_cmdline->parse());
3967             bool caught = false;
3968             try {
3969                 CPPUNIT_ASSERT(failure.m_cmdline->run());
3970             } catch (const StatusException &ex) {
3971                 if (!strcmp(ex.what(), "eds_event: no backend available")) {
3972                     caught = true;
3973                 } else {
3974                     throw;
3975                 }
3976             }
3977             CPPUNIT_ASSERT(caught);
3978         }
3979
3980         rm_r(m_testDir);
3981         {
3982             // allow user to proceed if they wish and possible: here eds_event is not configurable
3983             TestCmdline failure("--configure", "syncURL=local://@bar", "foo", "eds_event", NULL);
3984             CPPUNIT_ASSERT(failure.m_cmdline->parse());
3985             bool caught = false;
3986             try {
3987                 CPPUNIT_ASSERT(failure.m_cmdline->run());
3988             } catch (const StatusException &ex) {
3989                 if (!strcmp(ex.what(), "no such source(s): eds_event")) {
3990                     caught = true;
3991                 } else {
3992                     throw;
3993                 }
3994             }
3995             CPPUNIT_ASSERT(caught);
3996         }
3997
3998         rm_r(m_testDir);
3999         {
4000             // allow user to proceed if they wish and possible: here eds_event is not configurable (wrong context)
4001             TestCmdline failure("--configure", "syncURL=local://@bar", "eds_event/backend@xyz=calendar", "foo", "eds_event", NULL);
4002             CPPUNIT_ASSERT(failure.m_cmdline->parse());
4003             bool caught = false;
4004             try {
4005                 CPPUNIT_ASSERT(failure.m_cmdline->run());
4006             } catch (const StatusException &ex) {
4007                 if (!strcmp(ex.what(), "no such source(s): eds_event")) {
4008                     caught = true;
4009                 } else {
4010                     throw;
4011                 }
4012             }
4013             CPPUNIT_ASSERT(caught);
4014         }
4015
4016         rm_r(m_testDir);
4017         {
4018             // allow user to proceed if they wish: configure exactly the specified sources
4019             TestCmdline failure("--configure", "--template", "none", "backend=calendar", "foo", "eds_event", NULL);
4020             CPPUNIT_ASSERT(failure.m_cmdline->parse());
4021             CPPUNIT_ASSERT(failure.m_cmdline->run());
4022             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
4023             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_err.str());
4024
4025             string res = scanFiles(m_testDir);
4026             removeRandomUUID(res);
4027             CPPUNIT_ASSERT_EQUAL_DIFF(fooconfig + configsource, filterFiles(res));
4028         }
4029
4030         rm_r(m_testDir);
4031         {
4032             // allow user to proceed if they provide enough information: should result in no sources configured
4033             TestCmdline failure("--configure", "syncURL=local://@bar", "foo", NULL);
4034             CPPUNIT_ASSERT(failure.m_cmdline->parse());
4035             CPPUNIT_ASSERT(failure.m_cmdline->run());
4036             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
4037             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_err.str());
4038             string res = scanFiles(m_testDir);
4039             removeRandomUUID(res);
4040             CPPUNIT_ASSERT_EQUAL_DIFF(fooconfig + syncurl, filterFiles(res));
4041         }
4042
4043         rm_r(m_testDir);
4044         {
4045             // allow user to proceed if they provide enough information;
4046             // source created because listed and usable
4047             TestCmdline failure("--configure", "syncURL=local://@bar", "backend=calendar", "foo", "eds_event", NULL);
4048             CPPUNIT_ASSERT(failure.m_cmdline->parse());
4049             CPPUNIT_ASSERT(failure.m_cmdline->run());
4050             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
4051             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_err.str());
4052             string res = scanFiles(m_testDir);
4053             removeRandomUUID(res);
4054             CPPUNIT_ASSERT_EQUAL_DIFF(fooconfig + syncurl + configsource, filterFiles(res));
4055         }
4056
4057         rm_r(m_testDir);
4058         {
4059             // allow user to proceed if they provide enough information;
4060             // source created because listed and usable
4061             TestCmdline failure("--configure", "syncURL=local://@bar", "eds_event/backend@default=calendar", "foo", "eds_event", NULL);
4062             CPPUNIT_ASSERT(failure.m_cmdline->parse());
4063             CPPUNIT_ASSERT(failure.m_cmdline->run());
4064             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
4065             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_err.str());
4066             string res = scanFiles(m_testDir);
4067             removeRandomUUID(res);
4068             CPPUNIT_ASSERT_EQUAL_DIFF(fooconfig + syncurl + configsource, filterFiles(res));
4069         }
4070     }
4071
4072
4073     void testConfigureSources() {
4074         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
4075         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
4076         ScopedEnvChange home("HOME", m_testDir);
4077
4078         // create from scratch with only addressbook configured
4079         {
4080             TestCmdline cmdline("--configure",
4081                                 "--source-property", "database = file://tmp/test",
4082                                 "--source-property", "type = file:text/x-vcard",
4083                                 "@foobar",
4084                                 "addressbook",
4085                                 NULL);
4086             cmdline.doit();
4087         }
4088         string root = m_testDir;
4089         root += "/syncevolution/foobar";
4090         string res = scanFiles(root);
4091         removeRandomUUID(res);
4092         string expected =
4093             StringPrintf(".internal.ini:contextMinVersion = %d\n"
4094                          ".internal.ini:contextCurVersion = %d\n"
4095                          "config.ini:# logdir = \n"
4096                          "config.ini:# maxlogdirs = 10\n"
4097                          "config.ini:deviceId = fixed-devid\n"
4098                          "sources/addressbook/config.ini:backend = file\n"
4099                          "sources/addressbook/config.ini:database = file://tmp/test\n"
4100                          "sources/addressbook/config.ini:databaseFormat = text/x-vcard\n"
4101                          "sources/addressbook/config.ini:# databaseUser = \n"
4102                          "sources/addressbook/config.ini:# databasePassword = \n",
4103                          CONFIG_CONTEXT_MIN_VERSION,
4104                          CONFIG_CONTEXT_CUR_VERSION);
4105         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
4106
4107         // add calendar
4108         {
4109             TestCmdline cmdline("--configure",
4110                                 "--source-property", "database@foobar = file://tmp/test2",
4111                                 "--source-property", "backend = calendar",
4112                                 "@foobar",
4113                                 "calendar",
4114                                 NULL);
4115             cmdline.doit();
4116         }
4117         res = scanFiles(root);
4118         removeRandomUUID(res);
4119         expected +=
4120             "sources/calendar/config.ini:backend = calendar\n"
4121             "sources/calendar/config.ini:database = file://tmp/test2\n"
4122             "sources/calendar/config.ini:# databaseFormat = \n"
4123             "sources/calendar/config.ini:# databaseUser = \n"
4124             "sources/calendar/config.ini:# databasePassword = \n";
4125         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
4126
4127         // add ScheduleWorld peer: must reuse existing backend settings
4128         {
4129             TestCmdline cmdline("--configure",
4130                                 "scheduleworld@foobar",
4131                                 NULL);
4132             cmdline.doit();
4133         }
4134         res = scanFiles(root);
4135         removeRandomUUID(res);
4136         expected = ScheduleWorldConfig();
4137         boost::replace_all(expected,
4138                            "addressbook/config.ini:backend = addressbook",
4139                            "addressbook/config.ini:backend = file");
4140         boost::replace_all(expected,
4141                            "addressbook/config.ini:# database = ",
4142                            "addressbook/config.ini:database = file://tmp/test");
4143         boost::replace_all(expected,
4144                            "addressbook/config.ini:# databaseFormat = ",
4145                            "addressbook/config.ini:databaseFormat = text/x-vcard");
4146         boost::replace_all(expected,
4147                            "calendar/config.ini:# database = ",
4148                            "calendar/config.ini:database = file://tmp/test2");
4149         sortConfig(expected);
4150         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
4151
4152         // disable all sources except for addressbook
4153         {
4154             TestCmdline cmdline("--configure",
4155                                 "--source-property", "addressbook/sync=two-way",
4156                                 "--source-property", "sync=none",
4157                                 "scheduleworld@foobar",
4158                                 NULL);
4159             cmdline.doit();
4160         }
4161         res = scanFiles(root);
4162         removeRandomUUID(res);
4163         boost::replace_all(expected, "sync = two-way", "sync = disabled");
4164         boost::replace_first(expected, "sync = disabled", "sync = two-way");
4165         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
4166
4167         // override type in template while creating from scratch
4168         {
4169             TestCmdline cmdline("--configure",
4170                                 "--template", "SyncEvolution",
4171                                 "--source-property", "addressbook/type=file:text/vcard:3.0",
4172                                 "--source-property", "calendar/type=file:text/calendar:2.0",
4173                                 "syncevo@syncevo",
4174                                 NULL);
4175             cmdline.doit();
4176         }
4177         string syncevoroot = m_testDir + "/syncevolution/syncevo";
4178         res = scanFiles(syncevoroot + "/sources/addressbook");
4179         CPPUNIT_ASSERT(res.find("backend = file\n") != res.npos);
4180         CPPUNIT_ASSERT(res.find("databaseFormat = text/vcard\n") != res.npos);
4181         res = scanFiles(syncevoroot + "/sources/calendar");
4182         CPPUNIT_ASSERT(res.find("backend = file\n") != res.npos);
4183         CPPUNIT_ASSERT(res.find("databaseFormat = text/calendar\n") != res.npos);
4184     }
4185
4186     void testOldConfigure() {
4187         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
4188         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
4189         ScopedEnvChange home("HOME", m_testDir);
4190
4191         string oldConfig = OldScheduleWorldConfig();
4192         InitList<string> props = InitList<string>("serverNonce") +
4193             "clientNonce" +
4194             "devInfoHash" +
4195             "HashCode" +
4196             "ConfigDate" +
4197             "deviceData" +
4198             "adminData" +
4199             "synthesisID" +
4200             "rootMinVersion" +
4201             "rootCurVersion" +
4202             "contextMinVersion" +
4203             "contextCurVersion" +
4204             "peerMinVersion" +
4205             "peerCurVersion" +
4206             "lastNonce" +
4207             "last";
4208         BOOST_FOREACH(string &prop, props) {
4209             boost::replace_all(oldConfig,
4210                                prop + " = ",
4211                                prop + " = internal value");
4212         }
4213
4214         rm_r(m_testDir);
4215         createFiles(m_testDir + "/.sync4j/evolution/scheduleworld", oldConfig);
4216
4217         // Cannot read/and write old format anymore.
4218         SyncContext::setStableRelease(false);
4219         expectMigration("scheduleworld");
4220
4221         // Migrate explicitly.
4222         {
4223             TestCmdline cmdline("--migrate", "scheduleworld", NULL);
4224             cmdline.doit();
4225         }
4226
4227         // now test with new format
4228         string expected = ScheduleWorldConfig();
4229         boost::replace_first(expected, "# ConsumerReady = 0", "ConsumerReady = 1");
4230         boost::replace_first(expected, "# database = ", "database = xyz");
4231         boost::replace_first(expected, "# databaseUser = ", "databaseUser = foo");
4232         boost::replace_first(expected, "# databasePassword = ", "databasePassword = bar");
4233         // migrating "type" sets forceSyncFormat if not the default,
4234         // and databaseFormat (if format was part of type, as for addressbook)
4235         boost::replace_first(expected, "# databaseFormat = ", "databaseFormat = text/vcard");
4236         doConfigure(expected, "sources/addressbook/config.ini:");
4237     }
4238
4239     string doConfigure(const string &SWConfig, const string &addressbookPrefix) {
4240         string expected;
4241
4242         {
4243             TestCmdline cmdline("--configure",
4244                                 "--source-property", "sync = disabled",
4245                                 "scheduleworld",
4246                                 NULL);
4247             cmdline.doit();
4248             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4249             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4250             expected = filterConfig(internalToIni(SWConfig));
4251             boost::replace_all(expected,
4252                                "sync = two-way",
4253                                "sync = disabled");
4254             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
4255                                       filterConfig(printConfig("scheduleworld")));
4256         }
4257
4258         {
4259             TestCmdline cmdline("--configure",
4260                                 "--source-property", "sync = one-way-from-server",
4261                                 "scheduleworld",
4262                                 "addressbook",
4263                                 NULL);
4264             cmdline.doit();
4265             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4266             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4267             expected = SWConfig;
4268             boost::replace_all(expected,
4269                                "sync = two-way",
4270                                "sync = disabled");
4271             boost::replace_first(expected,
4272                                  addressbookPrefix + "sync = disabled",
4273                                  addressbookPrefix + "sync = one-way-from-server");
4274             expected = filterConfig(internalToIni(expected));
4275             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
4276                                       filterConfig(printConfig("scheduleworld")));
4277         }
4278
4279         {
4280             TestCmdline cmdline("--configure",
4281                                 "--sync", "two-way",
4282                                 "-z", "database=source",
4283                                 // note priority of suffix: most specific wins
4284                                 "--sync-property", "maxlogdirs@scheduleworld@default=20",
4285                                 "--sync-property", "maxlogdirs@default=10",
4286                                 "--sync-property", "maxlogdirs=5",
4287                                 "-y", "LOGDIR@default=logdir",
4288                                 "scheduleworld",
4289                                 NULL);
4290             cmdline.doit();
4291             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4292             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4293             boost::replace_all(expected,
4294                                "sync = one-way-from-server",
4295                                "sync = two-way");
4296             boost::replace_all(expected,
4297                                "sync = disabled",
4298                                "sync = two-way");
4299             boost::replace_all(expected,
4300                                "# database = ",
4301                                "database = source");
4302             boost::replace_all(expected,
4303                                "database = xyz",
4304                                "database = source");
4305             boost::replace_all(expected,
4306                                "# maxlogdirs = 10",
4307                                "maxlogdirs = 20");
4308             boost::replace_all(expected,
4309                                "# logdir = ",
4310                                "logdir = logdir");
4311             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
4312                                       filterConfig(printConfig("scheduleworld")));
4313         }
4314
4315         return expected;
4316     }
4317
4318     void testMigrate() {
4319         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
4320         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
4321         ScopedEnvChange home("HOME", m_testDir);
4322
4323         string oldRoot = m_testDir + "/.sync4j/evolution/scheduleworld";
4324         string newRoot = m_testDir + "/syncevolution/default";
4325
4326         string oldConfig = OldScheduleWorldConfig();
4327
4328         {
4329             // migrate old config
4330             createFiles(oldRoot, oldConfig);
4331             string createdConfig = scanFiles(oldRoot);
4332             TestCmdline cmdline("--migrate",
4333                                 "scheduleworld",
4334                                 NULL);
4335             cmdline.doit();
4336             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4337             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4338
4339             string migratedConfig = scanFiles(newRoot);
4340             string expected = ScheduleWorldConfig();
4341             sortConfig(expected);
4342             // migrating SyncEvolution < 1.2 configs sets
4343             // ConsumerReady, to keep config visible in the updated
4344             // sync-ui
4345             boost::replace_all(expected, "# ConsumerReady = 0", "ConsumerReady = 1");
4346             boost::replace_first(expected, "# database = ", "database = xyz");
4347             boost::replace_first(expected, "# databaseUser = ", "databaseUser = foo");
4348             boost::replace_first(expected, "# databasePassword = ", "databasePassword = bar");
4349             // migrating "type" sets forceSyncFormat if different from the "false" default
4350             // and databaseFormat (if format was part of type, as for addressbook)
4351             boost::replace_first(expected, "# databaseFormat = ", "databaseFormat = text/vcard");
4352             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4353             string renamedConfig = scanFiles(oldRoot + ".old");
4354             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
4355         }
4356
4357         {
4358             // rewrite existing config with obsolete properties
4359             // => these properties should get removed
4360             //
4361             // There is one limitation: shared nodes are not rewritten.
4362             // This is acceptable.
4363             createFiles(newRoot + "/peers/scheduleworld",
4364                         "config.ini:# obsolete comment\n"
4365                         "config.ini:obsoleteprop = foo\n",
4366                         true);
4367             string createdConfig = scanFiles(newRoot, "scheduleworld");
4368
4369             TestCmdline cmdline("--migrate",
4370                                 "scheduleworld",
4371                                 NULL);
4372             cmdline.doit();
4373             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4374             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4375
4376             string migratedConfig = scanFiles(newRoot, "scheduleworld");
4377             string expected = ScheduleWorldConfig();
4378             sortConfig(expected);
4379             boost::replace_all(expected, "# ConsumerReady = 0", "ConsumerReady = 1");
4380             boost::replace_first(expected, "# database = ", "database = xyz");
4381             boost::replace_first(expected, "# databaseUser = ", "databaseUser = foo");
4382             boost::replace_first(expected, "# databasePassword = ", "databasePassword = bar");
4383             boost::replace_first(expected, "# databaseFormat = ", "databaseFormat = text/vcard");
4384             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4385             string renamedConfig = scanFiles(newRoot, "scheduleworld.old.1");
4386             boost::replace_first(createdConfig, "ConsumerReady = 1", "ConsumerReady = 0");
4387             boost::replace_all(createdConfig, "/scheduleworld/", "/scheduleworld.old.1/");
4388             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
4389         }
4390
4391         {
4392             // migrate old config with changes and .synthesis directory, a second time
4393             createFiles(oldRoot, oldConfig);
4394             createFiles(oldRoot,
4395                         ".synthesis/dummy-file.bfi:dummy = foobar\n"
4396                         "spds/sources/addressbook/changes/config.txt:foo = bar\n"
4397                         "spds/sources/addressbook/changes/config.txt:foo2 = bar2\n",
4398                         true);
4399             string createdConfig = scanFiles(oldRoot);
4400             rm_r(newRoot);
4401             TestCmdline cmdline("--migrate",
4402                                 "scheduleworld",
4403                                 NULL);
4404             cmdline.doit();
4405             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4406             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4407
4408             string migratedConfig = scanFiles(newRoot);
4409             string expected = ScheduleWorldConfig();
4410             sortConfig(expected);
4411             boost::replace_all(expected, "# ConsumerReady = 0", "ConsumerReady = 1");
4412             boost::replace_first(expected, "# database = ", "database = xyz");
4413             boost::replace_first(expected, "# databaseUser = ", "databaseUser = foo");
4414             boost::replace_first(expected, "# databasePassword = ", "databasePassword = bar");
4415             boost::replace_first(expected, "# databaseFormat = ", "databaseFormat = text/vcard");
4416             boost::replace_first(expected,
4417                                  "peers/scheduleworld/sources/addressbook/config.ini",
4418                                  "peers/scheduleworld/sources/addressbook/.other.ini:foo = bar\n"
4419                                  "peers/scheduleworld/sources/addressbook/.other.ini:foo2 = bar2\n"
4420                                  "peers/scheduleworld/sources/addressbook/config.ini");
4421             boost::replace_first(expected,
4422                                  "peers/scheduleworld/config.ini",
4423                                  "peers/scheduleworld/.synthesis/dummy-file.bfi:dummy = foobar\n"
4424                                  "peers/scheduleworld/config.ini");
4425             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4426             string renamedConfig = scanFiles(oldRoot + ".old.1");
4427             boost::replace_first(createdConfig, "ConsumerReady = 1", "ConsumerReady = 0");
4428             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
4429         }
4430
4431         {
4432             string otherRoot = m_testDir + "/syncevolution/other";
4433             rm_r(otherRoot);
4434
4435             // migrate old config into non-default context
4436             createFiles(oldRoot, oldConfig);
4437             string createdConfig = scanFiles(oldRoot);
4438             {
4439                 TestCmdline cmdline("--migrate",
4440                                     "scheduleworld@other",
4441                                     NULL);
4442                 cmdline.doit();
4443                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4444                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4445             }
4446
4447             string migratedConfig = scanFiles(otherRoot);
4448             string expected = ScheduleWorldConfig();
4449             sortConfig(expected);
4450             boost::replace_all(expected, "# ConsumerReady = 0", "ConsumerReady = 1");
4451             boost::replace_first(expected, "# database = ", "database = xyz");
4452             boost::replace_first(expected, "# databaseUser = ", "databaseUser = foo");
4453             boost::replace_first(expected, "# databasePassword = ", "databasePassword = bar");
4454             boost::replace_first(expected, "# databaseFormat = ", "databaseFormat = text/vcard");
4455             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4456             string renamedConfig = scanFiles(oldRoot + ".old");
4457             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
4458
4459             // migrate the migrated config again inside the "other" context,
4460             // with no "default" context which might interfere with the tests
4461             //
4462             // ConsumerReady was set as part of previous migration,
4463             // must be removed during migration to hide the migrated
4464             // config from average users.
4465             rm_r(newRoot);
4466             {
4467                 TestCmdline cmdline("--migrate",
4468                                     "scheduleworld@other",
4469                                     NULL);
4470                 cmdline.doit();
4471                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4472                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4473             }
4474             migratedConfig = scanFiles(otherRoot, "scheduleworld");
4475             expected = ScheduleWorldConfig();
4476             sortConfig(expected);
4477             boost::replace_all(expected, "# ConsumerReady = 0", "ConsumerReady = 1");
4478             boost::replace_first(expected, "# database = ", "database = xyz");
4479             boost::replace_first(expected, "# databaseUser = ", "databaseUser = foo");
4480             boost::replace_first(expected, "# databasePassword = ", "databasePassword = bar");
4481             boost::replace_first(expected, "# databaseFormat = ", "databaseFormat = text/vcard");
4482             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4483             renamedConfig = scanFiles(otherRoot, "scheduleworld.old.3");
4484             boost::replace_all(expected, "/scheduleworld/", "/scheduleworld.old.3/");
4485             boost::replace_all(expected, "ConsumerReady = 1", "ConsumerReady = 0");
4486             CPPUNIT_ASSERT_EQUAL_DIFF(expected, renamedConfig);
4487
4488             // migrate once more, this time without the explicit context in
4489             // the config name => must not change the context, need second .old dir
4490             {
4491                 TestCmdline cmdline("--migrate",
4492                                     "scheduleworld",
4493                                     NULL);
4494                 cmdline.doit();
4495                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4496                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4497             }
4498             migratedConfig = scanFiles(otherRoot, "scheduleworld");
4499             boost::replace_all(expected, "/scheduleworld.old.3/", "/scheduleworld/");
4500             boost::replace_all(expected, "ConsumerReady = 0", "ConsumerReady = 1");          
4501             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4502             renamedConfig = scanFiles(otherRoot, "scheduleworld.old.4");
4503             boost::replace_all(expected, "/scheduleworld/", "/scheduleworld.old.4/");
4504             boost::replace_all(expected, "ConsumerReady = 1", "ConsumerReady = 0");
4505             CPPUNIT_ASSERT_EQUAL_DIFF(expected, renamedConfig);
4506
4507             // remove ConsumerReady: must be remain unset when migrating
4508             // hidden SyncEvolution >= 1.2 configs
4509             {
4510                 TestCmdline cmdline("--configure",
4511                                     "--sync-property", "ConsumerReady=0",
4512                                     "scheduleworld",
4513                                     NULL);
4514                 cmdline.doit();
4515                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4516                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4517             }
4518
4519             // migrate once more => keep ConsumerReady unset
4520             {
4521                 TestCmdline cmdline("--migrate",
4522                                     "scheduleworld",
4523                                     NULL);
4524                 cmdline.doit();
4525                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4526                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4527             }
4528             migratedConfig = scanFiles(otherRoot, "scheduleworld");
4529             boost::replace_all(expected, "/scheduleworld.old.4/", "/scheduleworld/");
4530             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4531             renamedConfig = scanFiles(otherRoot, "scheduleworld.old.5");
4532             boost::replace_all(expected, "/scheduleworld/", "/scheduleworld.old.5/");
4533             CPPUNIT_ASSERT_EQUAL_DIFF(expected, renamedConfig);
4534         }
4535     }
4536
4537     void testMigrateContext()
4538     {
4539         // Migrate context containing a peer. Must also migrate peer.
4540         // Covers special case of inconsistent "type".
4541
4542         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
4543         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
4544         ScopedEnvChange home("HOME", m_testDir);
4545
4546         string root = m_testDir + "/syncevolution/default";
4547
4548         string oldConfig =
4549             "config.ini:logDir = none\n"
4550             "peers/scheduleworld/config.ini:syncURL = http://sync.scheduleworld.com/funambol/ds\n"
4551             "peers/scheduleworld/config.ini:# username = \n"
4552             "peers/scheduleworld/config.ini:# password = \n"
4553
4554             "peers/scheduleworld/sources/addressbook/config.ini:sync = two-way\n"
4555             "peers/scheduleworld/sources/addressbook/config.ini:uri = card3\n"
4556             "peers/scheduleworld/sources/addressbook/config.ini:type = addressbook:text/vcard\n" // correct!
4557             "sources/addressbook/config.ini:type = calendar\n" // wrong!
4558
4559             "peers/funambol/config.ini:syncURL = http://sync.funambol.com/funambol/ds\n"
4560             "peers/funambol/config.ini:# username = \n"
4561             "peers/funambol/config.ini:# password = \n"
4562
4563             "peers/funambol/sources/calendar/config.ini:sync = refresh-from-server\n"
4564             "peers/funambol/sources/calendar/config.ini:uri = cal\n"
4565             "peers/funambol/sources/calendar/config.ini:type = calendar\n" // correct!
4566             "peers/funambol/sources/addressbook/config.ini:# sync = disabled\n"
4567             "peers/funambol/sources/addressbook/config.ini:type = file\n" // not used for context because source disabled
4568             "sources/calendar/config.ini:type = memos\n" // wrong!
4569
4570             "peers/memotoo/config.ini:syncURL = http://sync.memotoo.com/memotoo/ds\n"
4571             "peers/memotoo/config.ini:# username = \n"
4572             "peers/memotoo/config.ini:# password = \n"
4573
4574             "peers/memotoo/sources/memo/config.ini:sync = refresh-from-client\n"
4575             "peers/memotoo/sources/memo/config.ini:uri = cal\n"
4576             "peers/memotoo/sources/memo/config.ini:type = memo:text/plain\n" // correct!
4577             "sources/memo/config.ini:type = todo\n" // wrong!
4578             ;
4579
4580         {
4581             createFiles(root, oldConfig);
4582             TestCmdline cmdline("--migrate",
4583                                 "memo/backend=file", // override memo "backend" during migration
4584                                 "@default",
4585                                 NULL);
4586             cmdline.doit();
4587             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4588             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4589
4590             string migratedConfig = scanFiles(root);
4591             CPPUNIT_ASSERT(migratedConfig.find("peers/scheduleworld/") != migratedConfig.npos);
4592             CPPUNIT_ASSERT(migratedConfig.find("sources/addressbook/config.ini:backend = addressbook") != migratedConfig.npos);
4593             CPPUNIT_ASSERT(migratedConfig.find("sources/addressbook/config.ini:databaseFormat = text/vcard") != migratedConfig.npos);
4594             CPPUNIT_ASSERT(migratedConfig.find("peers/scheduleworld/sources/addressbook/config.ini:syncFormat = text/vcard") != migratedConfig.npos);
4595             CPPUNIT_ASSERT(migratedConfig.find("peers/scheduleworld/sources/addressbook/config.ini:sync = two-way") != migratedConfig.npos);
4596             CPPUNIT_ASSERT(migratedConfig.find("peers/scheduleworld/sources/calendar/config.ini:# sync = disabled") != migratedConfig.npos);
4597             CPPUNIT_ASSERT(migratedConfig.find("peers/scheduleworld/sources/memo/config.ini:# sync = disabled") != migratedConfig.npos);
4598             CPPUNIT_ASSERT(migratedConfig.find("sources/calendar/config.ini:backend = calendar") != migratedConfig.npos);
4599             CPPUNIT_ASSERT(migratedConfig.find("sources/calendar/config.ini:# databaseFormat = ") != migratedConfig.npos);
4600             CPPUNIT_ASSERT(migratedConfig.find("peers/funambol/sources/calendar/config.ini:# syncFormat = ") != migratedConfig.npos);
4601             CPPUNIT_ASSERT(migratedConfig.find("peers/funambol/sources/addressbook/config.ini:# sync = disabled") != migratedConfig.npos);
4602             CPPUNIT_ASSERT(migratedConfig.find("peers/funambol/sources/calendar/config.ini:sync = refresh-from-server") != migratedConfig.npos);
4603             CPPUNIT_ASSERT(migratedConfig.find("peers/funambol/sources/memo/config.ini:# sync = disabled") != migratedConfig.npos);
4604             CPPUNIT_ASSERT(migratedConfig.find("sources/memo/config.ini:backend = file") != migratedConfig.npos);
4605             CPPUNIT_ASSERT(migratedConfig.find("sources/memo/config.ini:databaseFormat = text/plain") != migratedConfig.npos);
4606             CPPUNIT_ASSERT(migratedConfig.find("peers/memotoo/sources/memo/config.ini:syncFormat = text/plain") != migratedConfig.npos);
4607             CPPUNIT_ASSERT(migratedConfig.find("peers/memotoo/sources/addressbook/config.ini:# sync = disabled") != migratedConfig.npos);
4608             CPPUNIT_ASSERT(migratedConfig.find("peers/memotoo/sources/calendar/config.ini:# sync = disabled") != migratedConfig.npos);
4609             CPPUNIT_ASSERT(migratedConfig.find("peers/memotoo/sources/memo/config.ini:sync = refresh-from-client") != migratedConfig.npos);
4610         }
4611     }
4612
4613     void testMigrateAutoSync() {
4614         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
4615         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
4616         ScopedEnvChange home("HOME", m_testDir);
4617
4618         string oldRoot = m_testDir + "/.sync4j/evolution/scheduleworld";
4619         string newRoot = m_testDir + "/syncevolution/default";
4620
4621         string oldConfig = "spds/syncml/config.txt:autoSync = 1\n";
4622         oldConfig += OldScheduleWorldConfig();
4623
4624         {
4625             // migrate old config
4626             createFiles(oldRoot, oldConfig);
4627             string createdConfig = scanFiles(oldRoot);
4628             TestCmdline cmdline("--migrate",
4629                                 "scheduleworld",
4630                                 NULL);
4631             cmdline.doit();
4632             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4633             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4634
4635             string migratedConfig = scanFiles(newRoot);
4636             string expected = ScheduleWorldConfig();
4637             boost::replace_first(expected, "# autoSync = 0", "autoSync = 1");
4638             sortConfig(expected);
4639             // migrating SyncEvolution < 1.2 configs sets
4640             // ConsumerReady, to keep config visible in the updated
4641             // sync-ui
4642             boost::replace_all(expected, "# ConsumerReady = 0", "ConsumerReady = 1");
4643             boost::replace_first(expected, "# database = ", "database = xyz");
4644             boost::replace_first(expected, "# databaseUser = ", "databaseUser = foo");
4645             boost::replace_first(expected, "# databasePassword = ", "databasePassword = bar");
4646             // migrating "type" sets forceSyncFormat if not already the default,
4647             // and databaseFormat (if format was part of type, as for addressbook)
4648             boost::replace_first(expected, "# databaseFormat = ", "databaseFormat = text/vcard");
4649             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4650             string renamedConfig = scanFiles(oldRoot + ".old");
4651             // autoSync must have been unset
4652             boost::replace_first(createdConfig, ":autoSync = 1", ":autoSync = 0");
4653             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
4654         }
4655
4656         {
4657             // rewrite existing config with autoSync set
4658             string createdConfig = scanFiles(newRoot, "scheduleworld");
4659
4660             TestCmdline cmdline("--migrate",
4661                                 "scheduleworld",
4662                                 NULL);
4663             cmdline.doit();
4664             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4665             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4666
4667             string migratedConfig = scanFiles(newRoot, "scheduleworld");
4668             string expected = ScheduleWorldConfig();
4669             boost::replace_first(expected, "# autoSync = 0", "autoSync = 1");
4670             sortConfig(expected);
4671             boost::replace_all(expected, "# ConsumerReady = 0", "ConsumerReady = 1");
4672             boost::replace_first(expected, "# database = ", "database = xyz");
4673             boost::replace_first(expected, "# databaseUser = ", "databaseUser = foo");
4674             boost::replace_first(expected, "# databasePassword = ", "databasePassword = bar");
4675             boost::replace_first(expected, "# databaseFormat = ", "databaseFormat = text/vcard");
4676             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
4677             string renamedConfig = scanFiles(newRoot, "scheduleworld.old.1");
4678             // autoSync must have been unset
4679             boost::replace_first(createdConfig, ":autoSync = 1", ":autoSync = 0");
4680             // the scheduleworld config was consumer ready, the migrated one isn't
4681             boost::replace_all(createdConfig, "ConsumerReady = 1", "ConsumerReady = 0");
4682             boost::replace_all(createdConfig, "/scheduleworld/", "/scheduleworld.old.1/");
4683             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
4684         }
4685     }
4686
4687     void testItemOperations() {
4688         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
4689         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
4690         ScopedEnvChange home("HOME", m_testDir);
4691
4692         {
4693             // "foo" not configured
4694             TestCmdline cmdline("--print-items",
4695                                 "foo",
4696                                 "bar",
4697                                 NULL);
4698             cmdline.doit(false);
4699             CPPUNIT_ASSERT_EQUAL_DIFF("[ERROR] bar: backend not supported or not correctly configured (backend=select backend databaseFormat= syncFormat=)\nconfiguration 'foo' does not exist\nsource 'bar' does not exist\nbackend property not set", cmdline.m_err.str());
4700             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4701         }
4702
4703         {
4704             // "foo" not configured, no source named
4705             TestCmdline cmdline("--print-items",
4706                                 "foo",
4707                                 NULL);
4708             cmdline.doit(false);
4709             CPPUNIT_ASSERT_EQUAL_DIFF("[ERROR] backend not supported or not correctly configured (backend=select backend databaseFormat= syncFormat=)\nconfiguration 'foo' does not exist\nno source selected\nbackend property not set", cmdline.m_err.str());
4710             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4711         }
4712
4713         {
4714             // nothing known about source
4715             TestCmdline cmdline("--print-items",
4716                                 NULL);
4717             cmdline.doit(false);
4718             CPPUNIT_ASSERT_EQUAL_DIFF("[ERROR] backend not supported or not correctly configured (backend=select backend databaseFormat= syncFormat=)\nno source selected\nbackend property not set", cmdline.m_err.str());
4719             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4720         }
4721
4722         {
4723             // now create foo
4724             TestCmdline cmdline("--configure",
4725                                 "--template",
4726                                 "default",
4727                                 "foo",
4728                                 NULL);
4729             cmdline.doit();
4730             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4731             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4732         }
4733
4734         {
4735             // "foo" now configured, still no source
4736             TestCmdline cmdline("--print-items",
4737                                 "foo",
4738                                 NULL);
4739             cmdline.doit(false);
4740             CPPUNIT_ASSERT_EQUAL_DIFF("[ERROR] backend not supported or not correctly configured (backend=select backend databaseFormat= syncFormat=)\nno source selected\nbackend property not set", cmdline.m_err.str());
4741             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4742         }
4743
4744         {
4745             // foo configured, but "bar" is not
4746             TestCmdline cmdline("--print-items",
4747                                 "foo",
4748                                 "bar",
4749                                 NULL);
4750             cmdline.doit(false);
4751             CPPUNIT_ASSERT_EQUAL_DIFF("[ERROR] bar: backend not supported or not correctly configured (backend=select backend databaseFormat= syncFormat=)\nsource 'bar' does not exist\nbackend property not set", cmdline.m_err.str());
4752             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4753         }
4754
4755         {
4756             // add "bar" source, using file backend
4757             TestCmdline cmdline("--configure",
4758                                 "backend=file",
4759                                 ("database=file://" + m_testDir + "/addressbook").c_str(),
4760                                 "databaseFormat=text/vcard",
4761                                 "foo",
4762                                 "bar",
4763                                 NULL);
4764             cmdline.doit();
4765             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4766             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4767         }
4768
4769         {
4770             // no items yet
4771             TestCmdline cmdline("--print-items",
4772                                 "foo",
4773                                 "bar",
4774                                 NULL);
4775             cmdline.doit();
4776             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4777             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
4778         }
4779
4780         static const std::string john =
4781             "BEGIN:VCARD\n"
4782             "VERSION:3.0\n"
4783             "FN:John Doe\n"
4784             "N:Doe;John;;;\n"
4785             "END:VCARD\n",
4786             joan =
4787             "BEGIN:VCARD\n"
4788             "VERSION:3.0\n"
4789             "FN:Joan Doe\n"
4790             "N:Doe;Joan;;;\n"
4791             "END:VCARD\n";
4792
4793         {
4794             // create one file
4795             std::string file1 = "1:" + john, file2 = "2:" + joan;
4796             boost::replace_all(file1, "\n", "\n1:");
4797             file1.resize(file1.size() - 2);
4798             boost::replace_all(file2, "\n", "\n2:");
4799             file2.resize(file2.size() - 2);
4800             createFiles(m_testDir + "/addressbook", file1 + file2);
4801
4802             TestCmdline cmdline("--print-items",
4803                                 "foo",
4804                                 "bar",
4805                                 NULL);
4806             cmdline.doit();
4807             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4808             CPPUNIT_ASSERT_EQUAL_DIFF("1\n2\n", cmdline.m_out.str());
4809         }
4810
4811         {
4812             // alternatively just specify enough parameters,
4813             // without the foo bar config part
4814             TestCmdline cmdline("--print-items",
4815                                 "backend=file",
4816                                 ("database=file://" + m_testDir + "/addressbook").c_str(),
4817                                 "databaseFormat=text/vcard",
4818                                 NULL);
4819             cmdline.doit();
4820             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4821             CPPUNIT_ASSERT_EQUAL_DIFF("1\n2\n", cmdline.m_out.str());
4822         }
4823
4824         {
4825             // export all
4826             TestCmdline cmdline("--export", "-",
4827                                 "backend=file",
4828                                 ("database=file://" + m_testDir + "/addressbook").c_str(),
4829                                 "databaseFormat=text/vcard",
4830                                 NULL);
4831             cmdline.doit();
4832             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4833             CPPUNIT_ASSERT_EQUAL_DIFF(john + "\n" + joan, cmdline.m_out.str());
4834         }
4835
4836         {
4837             // export all via config
4838             TestCmdline cmdline("--export", "-",
4839                                 "foo", "bar",
4840                                 NULL);
4841             cmdline.doit();
4842             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4843             CPPUNIT_ASSERT_EQUAL_DIFF(john + "\n" + joan, cmdline.m_out.str());
4844         }
4845
4846         {
4847             // export one
4848             TestCmdline cmdline("--export", "-",
4849                                 "backend=file",
4850                                 ("database=file://" + m_testDir + "/addressbook").c_str(),
4851                                 "databaseFormat=text/vcard",
4852                                 "--luids", "1",
4853                                 NULL);
4854             cmdline.doit();
4855             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4856             CPPUNIT_ASSERT_EQUAL_DIFF(john, cmdline.m_out.str());
4857         }
4858
4859         {
4860             // export one via config
4861             TestCmdline cmdline("--export", "-",
4862                                 "foo", "bar", "1",
4863                                 NULL);
4864             cmdline.doit();
4865             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
4866             CPPUNIT_ASSERT_EQUAL_DIFF(john, cmdline.m_out.str());
4867         }
4868
4869         // TODO: check configuration of just the source as @foo bar without peer
4870
4871         {
4872             // check error message for missing config name
4873             TestCmdline cmdline((const char *)NULL);
4874             cmdline.doit(false);
4875             CPPUNIT_ASSERT_NO_THROW(cmdline.expectUsageError("[ERROR] No configuration name specified.\n"));
4876         }
4877
4878         {
4879             // check error message for missing config name, version II
4880             TestCmdline cmdline("--run",
4881                                 NULL);
4882             cmdline.doit(false);
4883             CPPUNIT_ASSERT_NO_THROW(cmdline.expectUsageError("[ERROR] No configuration name specified.\n"));
4884         }
4885     }
4886
4887     const string m_testDir;        
4888
4889 private:
4890
4891     /**
4892      * vararg constructor with NULL termination,
4893      * out and error stream into stringstream members
4894      */
4895     class TestCmdline : private LoggerBase {
4896         void init() {
4897             pushLogger(this);
4898
4899             m_argv.reset(new const char *[m_argvstr.size() + 1]);
4900             m_argv[0] = "client-test";
4901             for (size_t index = 0;
4902                  index < m_argvstr.size();
4903                  ++index) {
4904                 m_argv[index + 1] = m_argvstr[index].c_str();
4905             }
4906
4907             m_cmdline.set(new KeyringSyncCmdline(m_argvstr.size() + 1, m_argv.get()), "cmdline");
4908         }
4909
4910     public:
4911         TestCmdline(const char *arg, ...) {
4912             va_list argList;
4913             va_start (argList, arg);
4914             for (const char *curr = arg;
4915                  curr;
4916                  curr = va_arg(argList, const char *)) {
4917                 m_argvstr.push_back(curr);
4918             }
4919             va_end(argList);
4920             init();
4921         }
4922
4923         TestCmdline(const char * const argv[]) {
4924             for (int i = 0; argv[i]; i++) {
4925                 m_argvstr.push_back(argv[i]);
4926             }
4927             init();
4928         }
4929
4930         ~TestCmdline() {
4931             popLogger();
4932         }
4933
4934         boost::shared_ptr<SyncContext> parse()
4935         {
4936             if (!m_cmdline->parse()) {
4937                 return boost::shared_ptr<SyncContext>();
4938             }
4939             boost::shared_ptr<SyncContext> context(new SyncContext(m_cmdline->m_server));
4940             context->setConfigFilter(true, "", m_cmdline->m_props.createSyncFilter(m_cmdline->m_server));
4941             return context;
4942         }
4943
4944         void doit(bool expectSuccess = true) {
4945             bool success = false;
4946             m_out.str("");
4947             m_err.str("");
4948             // emulates syncevolution.cpp exception handling
4949             try {
4950                 success = m_cmdline->parse() &&
4951                     m_cmdline->run();
4952             } catch (const std::exception &ex) {
4953                 m_err << "[ERROR] " << ex.what();
4954             } catch (...) {
4955                 std::string explanation;
4956                 Exception::handle(explanation);
4957                 m_err << "[ERROR] " << explanation;
4958             }
4959             if (expectSuccess && m_err.str().size()) {
4960                 m_out << endl << m_err.str();
4961             }
4962             CPPUNIT_ASSERT_MESSAGE(m_out.str(), success == expectSuccess);
4963         }
4964
4965         /** verify that Cmdline::usage() produced a short usage info followed by a specific error message */
4966         void expectUsageError(const std::string &error)
4967         {
4968             // expect short usage info as normal output
4969             std::string out = m_out.str();
4970             std::string err = m_err.str();
4971             std::string all = m_all.str();
4972             CPPUNIT_ASSERT(boost::starts_with(out, "List databases:\n"));
4973             CPPUNIT_ASSERT(out.find("\nOptions:\n") == std::string::npos);
4974             CPPUNIT_ASSERT(boost::ends_with(out,
4975                                             "Remove item(s):\n"
4976                                             "  syncevolution --delete-items [--] <config> <source> (<luid> ... | '*')\n\n"));
4977             // exact error message
4978             CPPUNIT_ASSERT_EQUAL(error, err);
4979
4980             // also check order
4981             CPPUNIT_ASSERT_EQUAL_DIFF(out + err, all);
4982         }
4983
4984         // separate streams for normal messages and error messages
4985         ostringstream m_out, m_err;
4986         // combined stream with all messages
4987         ostringstream m_all;
4988
4989         cxxptr<Cmdline> m_cmdline;
4990
4991     private:
4992         vector<string> m_argvstr;
4993         boost::scoped_array<const char *> m_argv;
4994
4995         /** capture output produced while test ran */
4996         void messagev(Level level,
4997                       const char *prefix,
4998                       const char *file,
4999                       int line,
5000                       const char *function,
5001                       const char *format,
5002                       va_list args)
5003         {
5004             if (level <= INFO) {
5005                 ostringstream &out = level != SHOW ? m_err : m_out;
5006                 std::string str = StringPrintfV(format, args);
5007                 if (level != SHOW) {
5008                     out << "[" << levelToStr(level) << "] ";
5009                     m_all << "[" << levelToStr(level) << "] ";
5010                 }
5011                 out << str;
5012                 m_all << str;
5013                 if (!boost::ends_with(str, "\n")) {
5014                     out << std::endl;
5015                     m_all << std::endl;
5016                 }
5017             }
5018         }
5019         virtual bool isProcessSafe() const { return false; }
5020     };
5021
5022     string DefaultConfig() {
5023         string config = ScheduleWorldConfig();
5024         boost::replace_first(config,
5025                              "syncURL = http://sync.scheduleworld.com/funambol/ds",
5026                              "syncURL = http://yourserver:port");
5027         boost::replace_first(config, "http://www.scheduleworld.com", "http://www.syncevolution.org");
5028         boost::replace_all(config, "ScheduleWorld", "SyncEvolution");
5029         boost::replace_all(config, "scheduleworld", "syncevolution");
5030         boost::replace_first(config, "PeerName = SyncEvolution", "# PeerName = ");
5031         boost::replace_first(config, "# ConsumerReady = 0", "ConsumerReady = 1");
5032         boost::replace_first(config, "uri = card3", "uri = addressbook");
5033         boost::replace_first(config, "uri = cal2", "uri = calendar");
5034         boost::replace_first(config, "uri = task2", "uri = todo");
5035         boost::replace_first(config, "uri = note", "uri = memo");
5036         boost::replace_first(config, "syncFormat = text/vcard", "# syncFormat = ");
5037         return config;
5038     }
5039
5040     string ScheduleWorldConfig(int contextMinVersion = CONFIG_CONTEXT_MIN_VERSION,
5041                                int contextCurVersion = CONFIG_CONTEXT_CUR_VERSION,
5042                                int peerMinVersion = CONFIG_PEER_MIN_VERSION,
5043                                int peerCurVersion = CONFIG_PEER_CUR_VERSION) {
5044         // properties sorted by the order in which they are defined
5045         // in the sync and sync source property registry
5046         string config =
5047             StringPrintf("peers/scheduleworld/.internal.ini:peerMinVersion = %d\n"
5048                          "peers/scheduleworld/.internal.ini:peerCurVersion = %d\n"
5049                          "peers/scheduleworld/.internal.ini:# HashCode = 0\n"
5050                          "peers/scheduleworld/.internal.ini:# ConfigDate = \n"
5051                          "peers/scheduleworld/.internal.ini:# lastNonce = \n"
5052                          "peers/scheduleworld/.internal.ini:# deviceData = \n"
5053                          "peers/scheduleworld/.internal.ini:# webDAVCredentialsOkay = 0\n"
5054                          "peers/scheduleworld/config.ini:syncURL = http://sync.scheduleworld.com/funambol/ds\n"
5055                          "peers/scheduleworld/config.ini:# username = \n"
5056                          "peers/scheduleworld/config.ini:# password = \n"
5057                          ".internal.ini:contextMinVersion = %d\n"
5058                          ".internal.ini:contextCurVersion = %d\n"
5059                          "config.ini:# logdir = \n"
5060                          "peers/scheduleworld/config.ini:# loglevel = 0\n"
5061                          "peers/scheduleworld/config.ini:# notifyLevel = 3\n"
5062                          "peers/scheduleworld/config.ini:# printChanges = 1\n"
5063                          "peers/scheduleworld/config.ini:# dumpData = 1\n"
5064                          "config.ini:# maxlogdirs = 10\n"
5065                          "peers/scheduleworld/config.ini:# autoSync = 0\n"
5066                          "peers/scheduleworld/config.ini:# autoSyncInterval = 30M\n"
5067                          "peers/scheduleworld/config.ini:# autoSyncDelay = 5M\n"
5068                          "peers/scheduleworld/config.ini:# preventSlowSync = 1\n"
5069                          "peers/scheduleworld/config.ini:# useProxy = 0\n"
5070                          "peers/scheduleworld/config.ini:# proxyHost = \n"
5071                          "peers/scheduleworld/config.ini:# proxyUsername = \n"
5072                          "peers/scheduleworld/config.ini:# proxyPassword = \n"
5073                          "peers/scheduleworld/config.ini:# clientAuthType = md5\n"
5074                          "peers/scheduleworld/config.ini:# RetryDuration = 5M\n"
5075                          "peers/scheduleworld/config.ini:# RetryInterval = 2M\n"
5076                          "peers/scheduleworld/config.ini:# remoteIdentifier = \n"
5077                          "peers/scheduleworld/config.ini:# PeerIsClient = 0\n"
5078                          "peers/scheduleworld/config.ini:# SyncMLVersion = \n"
5079                          "peers/scheduleworld/config.ini:PeerName = ScheduleWorld\n"
5080                          "config.ini:deviceId = fixed-devid\n" /* this is not the default! */
5081                          "peers/scheduleworld/config.ini:# remoteDeviceId = \n"
5082                          "peers/scheduleworld/config.ini:# enableWBXML = 1\n"
5083                          "peers/scheduleworld/config.ini:# maxMsgSize = 150000\n"
5084                          "peers/scheduleworld/config.ini:# maxObjSize = 4000000\n"
5085                          "peers/scheduleworld/config.ini:# SSLServerCertificates = \n"
5086                          "peers/scheduleworld/config.ini:# SSLVerifyServer = 1\n"
5087                          "peers/scheduleworld/config.ini:# SSLVerifyHost = 1\n"
5088                          "peers/scheduleworld/config.ini:WebURL = http://www.scheduleworld.com\n"
5089                          "peers/scheduleworld/config.ini:IconURI = image://themedimage/icons/services/scheduleworld\n"
5090                          "peers/scheduleworld/config.ini:# ConsumerReady = 0\n"
5091                          "peers/scheduleworld/config.ini:# peerType = \n"
5092
5093                          "peers/scheduleworld/sources/addressbook/.internal.ini:# adminData = \n"
5094                          "peers/scheduleworld/sources/addressbook/.internal.ini:# synthesisID = 0\n"
5095                          "peers/scheduleworld/sources/addressbook/config.ini:sync = two-way\n"
5096                          "peers/scheduleworld/sources/addressbook/config.ini:uri = card3\n"
5097                          "sources/addressbook/config.ini:backend = addressbook\n"
5098                          "peers/scheduleworld/sources/addressbook/config.ini:syncFormat = text/vcard\n"
5099                          "peers/scheduleworld/sources/addressbook/config.ini:# forceSyncFormat = 0\n"
5100                          "sources/addressbook/config.ini:# database = \n"
5101                          "sources/addressbook/config.ini:# databaseFormat = \n"
5102                          "sources/addressbook/config.ini:# databaseUser = \n"
5103                          "sources/addressbook/config.ini:# databasePassword = \n"
5104
5105                          "peers/scheduleworld/sources/calendar/.internal.ini:# adminData = \n"
5106                          "peers/scheduleworld/sources/calendar/.internal.ini:# synthesisID = 0\n"
5107                          "peers/scheduleworld/sources/calendar/config.ini:sync = two-way\n"
5108                          "peers/scheduleworld/sources/calendar/config.ini:uri = cal2\n"
5109                          "sources/calendar/config.ini:backend = calendar\n"
5110                          "peers/scheduleworld/sources/calendar/config.ini:# syncFormat = \n"
5111                          "peers/scheduleworld/sources/calendar/config.ini:# forceSyncFormat = 0\n"
5112                          "sources/calendar/config.ini:# database = \n"
5113                          "sources/calendar/config.ini:# databaseFormat = \n"
5114                          "sources/calendar/config.ini:# databaseUser = \n"
5115                          "sources/calendar/config.ini:# databasePassword = \n"
5116
5117                          "peers/scheduleworld/sources/memo/.internal.ini:# adminData = \n"
5118                          "peers/scheduleworld/sources/memo/.internal.ini:# synthesisID = 0\n"
5119                          "peers/scheduleworld/sources/memo/config.ini:sync = two-way\n"
5120                          "peers/scheduleworld/sources/memo/config.ini:uri = note\n"
5121                          "sources/memo/config.ini:backend = memo\n"
5122                          "peers/scheduleworld/sources/memo/config.ini:# syncFormat = \n"
5123                          "peers/scheduleworld/sources/memo/config.ini:# forceSyncFormat = 0\n"
5124                          "sources/memo/config.ini:# database = \n"
5125                          "sources/memo/config.ini:# databaseFormat = \n"
5126                          "sources/memo/config.ini:# databaseUser = \n"
5127                          "sources/memo/config.ini:# databasePassword = \n"
5128
5129                          "peers/scheduleworld/sources/todo/.internal.ini:# adminData = \n"
5130                          "peers/scheduleworld/sources/todo/.internal.ini:# synthesisID = 0\n"
5131                          "peers/scheduleworld/sources/todo/config.ini:sync = two-way\n"
5132                          "peers/scheduleworld/sources/todo/config.ini:uri = task2\n"
5133                          "sources/todo/config.ini:backend = todo\n"
5134                          "peers/scheduleworld/sources/todo/config.ini:# syncFormat = \n"
5135                          "peers/scheduleworld/sources/todo/config.ini:# forceSyncFormat = 0\n"
5136                          "sources/todo/config.ini:# database = \n"
5137                          "sources/todo/config.ini:# databaseFormat = \n"
5138                          "sources/todo/config.ini:# databaseUser = \n"
5139                          "sources/todo/config.ini:# databasePassword = ",
5140                          peerMinVersion, peerCurVersion,
5141                          contextMinVersion, contextCurVersion);
5142 #ifdef ENABLE_LIBSOUP
5143         // path to SSL certificates has to be set only for libsoup
5144         boost::replace_first(config,
5145                              "SSLServerCertificates = ",
5146                              "SSLServerCertificates = /etc/ssl/certs/ca-certificates.crt:/etc/pki/tls/certs/ca-bundle.crt:/usr/share/ssl/certs/ca-bundle.crt");
5147 #endif
5148
5149 #if 0
5150         // Currently we don't have an icon for ScheduleWorld. If we
5151         // had (MB #2062) one, then this code would ensure that the
5152         // reference config also has the right path for it.
5153         const char *templateDir = getenv("SYNCEVOLUTION_TEMPLATE_DIR");
5154         if (!templateDir) {
5155             templateDir = TEMPLATE_DIR;
5156         }
5157
5158
5159         if (isDir(string(templateDir) + "/ScheduleWorld")) {
5160             boost::replace_all(config,
5161                                "# IconURI = ",
5162                                string("IconURI = file://") + templateDir + "/ScheduleWorld/icon.png");
5163         }
5164 #endif
5165         return config;
5166     }
5167
5168     string OldScheduleWorldConfig() {
5169         // old style paths
5170         string oldConfig =
5171             "spds/syncml/config.txt:syncURL = http://sync.scheduleworld.com/funambol/ds\n"
5172             "spds/syncml/config.txt:# username = \n"
5173             "spds/syncml/config.txt:# password = \n"
5174             "spds/syncml/config.txt:# logdir = \n"
5175             "spds/syncml/config.txt:# loglevel = 0\n"
5176             "spds/syncml/config.txt:# notifyLevel = 3\n"
5177             "spds/syncml/config.txt:# printChanges = 1\n"
5178             "spds/syncml/config.txt:# dumpData = 1\n"
5179             "spds/syncml/config.txt:# maxlogdirs = 10\n"
5180             "spds/syncml/config.txt:# autoSync = 0\n"
5181             "spds/syncml/config.txt:# autoSyncInterval = 30M\n"
5182             "spds/syncml/config.txt:# autoSyncDelay = 5M\n"
5183             "spds/syncml/config.txt:# preventSlowSync = 1\n"
5184             "spds/syncml/config.txt:# useProxy = 0\n"
5185             "spds/syncml/config.txt:# proxyHost = \n"
5186             "spds/syncml/config.txt:# proxyUsername = \n"
5187             "spds/syncml/config.txt:# proxyPassword = \n"
5188             "spds/syncml/config.txt:# clientAuthType = md5\n"
5189             "spds/syncml/config.txt:# RetryDuration = 5M\n"
5190             "spds/syncml/config.txt:# RetryInterval = 2M\n"
5191             "spds/syncml/config.txt:# remoteIdentifier = \n"
5192             "spds/syncml/config.txt:# PeerIsClient = 0\n"
5193             "spds/syncml/config.txt:# SyncMLVersion = \n"
5194             "spds/syncml/config.txt:PeerName = ScheduleWorld\n"
5195             "spds/syncml/config.txt:deviceId = fixed-devid\n" /* this is not the default! */
5196             "spds/syncml/config.txt:# remoteDeviceId = \n"
5197             "spds/syncml/config.txt:# enableWBXML = 1\n"
5198             "spds/syncml/config.txt:# maxMsgSize = 150000\n"
5199             "spds/syncml/config.txt:# maxObjSize = 4000000\n"
5200 #ifdef ENABLE_LIBSOUP
5201             // path to SSL certificates is only set for libsoup
5202             "spds/syncml/config.txt:# SSLServerCertificates = /etc/ssl/certs/ca-certificates.crt:/etc/pki/tls/certs/ca-bundle.crt:/usr/share/ssl/certs/ca-bundle.crt\n"
5203
5204 #else
5205             "spds/syncml/config.txt:# SSLServerCertificates = \n"
5206 #endif
5207             "spds/syncml/config.txt:# SSLVerifyServer = 1\n"
5208             "spds/syncml/config.txt:# SSLVerifyHost = 1\n"
5209             "spds/syncml/config.txt:WebURL = http://www.scheduleworld.com\n"
5210             "spds/syncml/config.txt:IconURI = image://themedimage/icons/services/scheduleworld\n"
5211             "spds/syncml/config.txt:# ConsumerReady = 0\n"
5212             "spds/sources/addressbook/config.txt:sync = two-way\n"
5213             "spds/sources/addressbook/config.txt:type = addressbook:text/vcard\n"
5214             "spds/sources/addressbook/config.txt:evolutionsource = xyz\n"
5215             "spds/sources/addressbook/config.txt:uri = card3\n"
5216             "spds/sources/addressbook/config.txt:evolutionuser = foo\n"
5217             "spds/sources/addressbook/config.txt:evolutionpassword = bar\n"
5218             "spds/sources/calendar/config.txt:sync = two-way\n"
5219             "spds/sources/calendar/config.txt:type = calendar\n"
5220             "spds/sources/calendar/config.txt:# database = \n"
5221             "spds/sources/calendar/config.txt:uri = cal2\n"
5222             "spds/sources/calendar/config.txt:# evolutionuser = \n"
5223             "spds/sources/calendar/config.txt:# evolutionpassword = \n"
5224             "spds/sources/memo/config.txt:sync = two-way\n"
5225             "spds/sources/memo/config.txt:type = memo\n"
5226             "spds/sources/memo/config.txt:# database = \n"
5227             "spds/sources/memo/config.txt:uri = note\n"
5228             "spds/sources/memo/config.txt:# evolutionuser = \n"
5229             "spds/sources/memo/config.txt:# evolutionpassword = \n"
5230             "spds/sources/todo/config.txt:sync = two-way\n"
5231             "spds/sources/todo/config.txt:type = todo\n"
5232             "spds/sources/todo/config.txt:# database = \n"
5233             "spds/sources/todo/config.txt:uri = task2\n"
5234             "spds/sources/todo/config.txt:# evolutionuser = \n"
5235             "spds/sources/todo/config.txt:# evolutionpassword = \n";
5236         return oldConfig;
5237     }
5238
5239     string FunambolConfig() {
5240         string config = ScheduleWorldConfig();
5241         boost::replace_all(config, "/scheduleworld/", "/funambol/");
5242         boost::replace_all(config, "PeerName = ScheduleWorld", "PeerName = Funambol");
5243
5244         boost::replace_first(config,
5245                              "syncURL = http://sync.scheduleworld.com/funambol/ds",
5246                              "syncURL = http://my.funambol.com/sync");
5247
5248         boost::replace_first(config,
5249                              "WebURL = http://www.scheduleworld.com",
5250                              "WebURL = http://my.funambol.com");
5251
5252         boost::replace_first(config,
5253                              "IconURI = image://themedimage/icons/services/scheduleworld",
5254                              "IconURI = image://themedimage/icons/services/funambol");
5255
5256         boost::replace_first(config,
5257                              "# ConsumerReady = 0",
5258                              "ConsumerReady = 1");
5259
5260         boost::replace_first(config,
5261                              "# enableWBXML = 1",
5262                              "enableWBXML = 0");
5263
5264         boost::replace_first(config,
5265                              "# RetryInterval = 2M",
5266                              "RetryInterval = 0");
5267
5268         boost::replace_first(config,
5269                              "addressbook/config.ini:uri = card3",
5270                              "addressbook/config.ini:uri = card");
5271         boost::replace_all(config,
5272                            "addressbook/config.ini:syncFormat = text/vcard",
5273                            "addressbook/config.ini:# syncFormat = ");
5274
5275         boost::replace_first(config,
5276                              "calendar/config.ini:uri = cal2",
5277                              "calendar/config.ini:uri = event");
5278         boost::replace_all(config,
5279                            "calendar/config.ini:# syncFormat = ",
5280                            "calendar/config.ini:syncFormat = text/calendar");
5281         boost::replace_all(config,
5282                            "calendar/config.ini:# forceSyncFormat = 0",
5283                            "calendar/config.ini:forceSyncFormat = 1");
5284
5285         boost::replace_first(config,
5286                              "todo/config.ini:uri = task2",
5287                              "todo/config.ini:uri = task");
5288         boost::replace_all(config,
5289                            "todo/config.ini:# syncFormat = ",
5290                            "todo/config.ini:syncFormat = text/calendar");
5291         boost::replace_all(config,
5292                            "todo/config.ini:# forceSyncFormat = 0",
5293                            "todo/config.ini:forceSyncFormat = 1");
5294
5295         return config;
5296     }
5297
5298     string SynthesisConfig() {
5299         string config = ScheduleWorldConfig();
5300         boost::replace_all(config, "/scheduleworld/", "/synthesis/");
5301         boost::replace_all(config, "PeerName = ScheduleWorld", "PeerName = Synthesis");
5302
5303         boost::replace_first(config,
5304                              "syncURL = http://sync.scheduleworld.com/funambol/ds",
5305                              "syncURL = http://www.synthesis.ch/sync");
5306
5307         boost::replace_first(config,
5308                              "WebURL = http://www.scheduleworld.com",
5309                              "WebURL = http://www.synthesis.ch");
5310
5311         boost::replace_first(config,
5312                              "IconURI = image://themedimage/icons/services/scheduleworld",
5313                              "IconURI = image://themedimage/icons/services/synthesis");
5314
5315         boost::replace_first(config,
5316                              "addressbook/config.ini:uri = card3",
5317                              "addressbook/config.ini:uri = contacts");
5318         boost::replace_all(config,
5319                            "addressbook/config.ini:syncFormat = text/vcard",
5320                            "addressbook/config.ini:# syncFormat = ");
5321
5322         boost::replace_first(config,
5323                              "calendar/config.ini:uri = cal2",
5324                              "calendar/config.ini:uri = events");
5325         boost::replace_first(config,
5326                              "calendar/config.ini:sync = two-way",
5327                              "calendar/config.ini:sync = disabled");
5328
5329         boost::replace_first(config,
5330                              "memo/config.ini:uri = note",
5331                              "memo/config.ini:uri = notes");
5332
5333         boost::replace_first(config,
5334                              "todo/config.ini:uri = task2",
5335                              "todo/config.ini:uri = tasks");
5336         boost::replace_first(config,
5337                              "todo/config.ini:sync = two-way",
5338                              "todo/config.ini:sync = disabled");
5339
5340         return config;
5341     }          
5342
5343     /** create directory hierarchy, overwriting previous content */
5344     void createFiles(const string &root, const string &content, bool append = false) {
5345         if (!append) {
5346             rm_r(root);
5347         }
5348
5349         size_t start = 0;
5350         ofstream out;
5351         string outname;
5352
5353         out.exceptions(ios_base::badbit|ios_base::failbit);
5354         while (start < content.size()) {
5355             size_t delim = content.find(':', start);
5356             size_t end = content.find('\n', start);
5357             if (delim == content.npos ||
5358                 end == content.npos) {
5359                 // invalid content ?!
5360                 break;
5361             }
5362             string newname = content.substr(start, delim - start);
5363             string line = content.substr(delim + 1, end - delim - 1);
5364             if (newname != outname) {
5365                 if (out.is_open()) {
5366                     out.close();
5367                 }
5368                 string fullpath = root + "/" + newname;
5369                 size_t fileoff = fullpath.rfind('/');
5370                 mkdir_p(fullpath.substr(0, fileoff));
5371                 out.open(fullpath.c_str(),
5372                          append ? (ios_base::out|ios_base::ate|ios_base::app) : (ios_base::out|ios_base::trunc));
5373                 outname = newname;
5374             }
5375             out << line << endl;
5376             start = end + 1;
5377         }
5378     }
5379
5380     /** turn directory hierarchy into string
5381      *
5382      * @param root       root path in file system
5383      * @param peer       if non-empty, then ignore all <root>/peers/<foo> directories
5384      *                   where <foo> != peer
5385      * @param onlyProps  ignore lines which are comments
5386      */
5387     string scanFiles(const string &root, const string &peer = "", bool onlyProps = true) {
5388         ostringstream out;
5389
5390         scanFiles(root, "", peer, out, onlyProps);
5391         return out.str();
5392     }
5393
5394     void scanFiles(const string &root, const string &dir, const string &peer, ostringstream &out, bool onlyProps) {
5395         string newroot = root;
5396         newroot += "/";
5397         newroot += dir;
5398         ReadDir readDir(newroot);
5399         sort(readDir.begin(), readDir.end());
5400
5401         BOOST_FOREACH(const string &entry, readDir) {
5402             if (isDir(newroot + "/" + entry)) {
5403                 if (boost::ends_with(newroot, "/peers") &&
5404                     !peer.empty() &&
5405                     entry != peer) {
5406                     // skip different peer directory
5407                     continue;
5408                 } else {
5409                     scanFiles(root, dir + (dir.empty() ? "" : "/") + entry, peer, out, onlyProps);
5410                 }
5411             } else {
5412                 ifstream in;
5413                 in.exceptions(ios_base::badbit /* failbit must not trigger exception because is set when reaching eof ?! */);
5414                 in.open((newroot + "/" + entry).c_str());
5415                 string line;
5416                 while (!in.eof()) {
5417                     getline(in, line);
5418                     if ((line.size() || !in.eof()) && 
5419                         (!onlyProps ||
5420                          (boost::starts_with(line, "# ") ?
5421                           isPropAssignment(line.substr(2)) :
5422                           !line.empty()))) {
5423                         if (dir.size()) {
5424                             out << dir << "/";
5425                         }
5426                         out << entry << ":";
5427                         out << line << '\n';
5428                     }
5429                 }
5430             }
5431         }
5432     }
5433
5434     string printConfig(const string &server) {
5435         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "templates");
5436         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
5437         ScopedEnvChange home("HOME", m_testDir);
5438
5439         TestCmdline cmdline("--print-config", server.c_str(), NULL);
5440         cmdline.doit();
5441         CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
5442         return cmdline.m_out.str();
5443     }
5444 };
5445
5446 SYNCEVOLUTION_TEST_SUITE_REGISTRATION(CmdlineTest);
5447
5448 #endif // ENABLE_UNIT_TESTS
5449
5450 SE_END_CXX