Imported Upstream version 1.0beta3
[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/FilterConfigNode.h>
23 #include <syncevo/VolatileConfigNode.h>
24 #include <syncevo/SyncSource.h>
25 #include <syncevo/SyncContext.h>
26 #include <syncevo/util.h>
27 #include "test.h"
28
29 #include <unistd.h>
30 #include <errno.h>
31
32 #include <iostream>
33 #include <iomanip>
34 #include <sstream>
35 #include <memory>
36 #include <set>
37 #include <list>
38 #include <algorithm>
39 using namespace std;
40
41 #include <boost/shared_ptr.hpp>
42 #include <boost/algorithm/string/join.hpp>
43 #include <boost/algorithm/string.hpp>
44 #include <boost/foreach.hpp>
45
46 #include <syncevo/declarations.h>
47 SE_BEGIN_CXX
48
49 Cmdline::Cmdline(int argc, const char * const * argv, ostream &out, ostream &err) :
50     m_argc(argc),
51     m_argv(argv),
52     m_out(out),
53     m_err(err),
54     m_validSyncProps(SyncConfig::getRegistry()),
55     m_validSourceProps(SyncSourceConfig::getRegistry())
56 {}
57
58 Cmdline::Cmdline(const vector<string> &args, ostream &out, ostream &err) :
59     m_args(args),
60     m_out(out),
61     m_err(err),
62     m_validSyncProps(SyncConfig::getRegistry()),
63     m_validSourceProps(SyncSourceConfig::getRegistry())
64 {
65     m_argc = args.size();
66     m_argvArray.reset(new const char *[args.size()]);
67     for(int i = 0; i < m_argc; i++) {
68         m_argvArray[i] = m_args[i].c_str();
69     }
70     m_argv = m_argvArray.get();
71 }
72
73 bool Cmdline::parse()
74 {
75     vector<string> parsed;
76     return parse(parsed);
77 }
78
79 bool Cmdline::parse(vector<string> &parsed)
80 {
81     parsed.clear();
82     if (m_argc) {
83         parsed.push_back(m_argv[0]);
84     }
85
86     int opt = 1;
87     bool ok;
88     while (opt < m_argc) {
89         parsed.push_back(m_argv[opt]);
90         if (m_argv[opt][0] != '-') {
91             break;
92         }
93         if (boost::iequals(m_argv[opt], "--sync") ||
94             boost::iequals(m_argv[opt], "-s")) {
95             opt++;
96             string param;
97             string cmdopt(m_argv[opt - 1]);
98             if (!parseProp(m_validSourceProps, m_sourceProps,
99                            m_argv[opt - 1], opt == m_argc ? NULL : m_argv[opt],
100                            SyncSourceConfig::m_sourcePropSync.getName().c_str())) {
101                 return false;
102             }
103             parsed.push_back(m_argv[opt]);
104
105             // disable requirement to add --run explicitly in order to
106             // be compatible with traditional command lines
107             m_run = true;
108         } else if(boost::iequals(m_argv[opt], "--sync-property") ||
109                   boost::iequals(m_argv[opt], "-y")) {
110                 opt++;
111                 if (!parseProp(m_validSyncProps, m_syncProps,
112                                m_argv[opt - 1], opt == m_argc ? NULL : m_argv[opt])) {
113                     return false;
114                 }
115                 parsed.push_back(m_argv[opt]);
116         } else if(boost::iequals(m_argv[opt], "--source-property") ||
117                   boost::iequals(m_argv[opt], "-z")) {
118             opt++;
119             if (!parseProp(m_validSourceProps, m_sourceProps,
120                            m_argv[opt - 1], opt == m_argc ? NULL : m_argv[opt])) {
121                 return false;
122             }
123             parsed.push_back(m_argv[opt]);
124         }else if(boost::iequals(m_argv[opt], "--template") ||
125                   boost::iequals(m_argv[opt], "-l")) {
126             opt++;
127             if (opt >= m_argc) {
128                 usage(true, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
129                 return false;
130             }
131             parsed.push_back(m_argv[opt]);
132             m_template = m_argv[opt];
133             m_configure = true;
134             string temp = boost::trim_copy (m_template);
135             if (temp.find ("?") == 0){
136                 m_printTemplates = true;
137                 m_dontrun = true;
138                 m_template = temp.substr (1);
139             }
140         } else if(boost::iequals(m_argv[opt], "--print-servers") ||
141                   boost::iequals(m_argv[opt], "--print-peers") ||
142                   boost::iequals(m_argv[opt], "--print-configs")) {
143             m_printServers = true;
144         } else if(boost::iequals(m_argv[opt], "--print-config") ||
145                   boost::iequals(m_argv[opt], "-p")) {
146             m_printConfig = true;
147         } else if(boost::iequals(m_argv[opt], "--print-sessions")) {
148             m_printSessions = true;
149         } else if(boost::iequals(m_argv[opt], "--configure") ||
150                   boost::iequals(m_argv[opt], "-c")) {
151             m_configure = true;
152         } else if(boost::iequals(m_argv[opt], "--remove")) {
153             m_remove = true;
154         } else if(boost::iequals(m_argv[opt], "--run") ||
155                   boost::iequals(m_argv[opt], "-r")) {
156             m_run = true;
157         } else if(boost::iequals(m_argv[opt], "--restore")) {
158             opt++;
159             if (opt >= m_argc) {
160                 usage(true, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
161                 return false;
162             }
163             m_restore = m_argv[opt];
164             if (m_restore.empty()) {
165                 usage(true, string("missing parameter for ") + cmdOpt(m_argv[opt - 1]));
166                 return false;
167             }
168             //if can't convert it successfully, it's an invalid path
169             if (!relToAbs(m_restore)) {
170                 usage(true, string("parameter '") + m_restore + "' for " + cmdOpt(m_argv[opt - 1]) + " must be log directory");
171                 return false;
172             }
173             parsed.push_back(m_restore);
174         } else if(boost::iequals(m_argv[opt], "--before")) {
175             m_before = true;
176         } else if(boost::iequals(m_argv[opt], "--after")) {
177             m_after = true;
178         } else if(boost::iequals(m_argv[opt], "--dry-run")) {
179             m_dryrun = true;
180         } else if(boost::iequals(m_argv[opt], "--migrate")) {
181             m_migrate = true;
182         } else if(boost::iequals(m_argv[opt], "--status") ||
183                   boost::iequals(m_argv[opt], "-t")) {
184             m_status = true;
185         } else if(boost::iequals(m_argv[opt], "--quiet") ||
186                   boost::iequals(m_argv[opt], "-q")) {
187             m_quiet = true;
188         } else if(boost::iequals(m_argv[opt], "--help") ||
189                   boost::iequals(m_argv[opt], "-h")) {
190             m_usage = true;
191         } else if(boost::iequals(m_argv[opt], "--version")) {
192             m_version = true;
193         } else if (parseBool(opt, "--keyring", "-k", true, m_keyring, ok)) {
194             if (!ok) {
195                 return false;
196             }
197         } else if (parseBool(opt, "--daemon", NULL, true, m_useDaemon, ok)) {
198             if (!ok) {
199                 return false;
200             }
201         } else if(boost::iequals(m_argv[opt], "--monitor")||
202                 boost::iequals(m_argv[opt], "-m")) {
203             m_monitor = true;
204         } else {
205             usage(false, string(m_argv[opt]) + ": unknown parameter");
206             return false;
207         }
208         opt++;
209     }
210
211     if (opt < m_argc) {
212         m_server = m_argv[opt++];
213         while (opt < m_argc) {
214             parsed.push_back(m_argv[opt]);
215             m_sources.insert(m_argv[opt++]);
216         }
217     }
218
219     return true;
220 }
221
222 bool Cmdline::parseBool(int opt, const char *longName, const char *shortName,
223                         bool def, Bool &value,
224                         bool &ok)
225 {
226     string option = m_argv[opt];
227     string param;
228     size_t pos = option.find('=');
229     if (pos != option.npos) {
230         param = option.substr(pos + 1);
231         option.resize(pos);
232     }
233     if ((longName && boost::iequals(option, longName)) ||
234         (shortName && boost::iequals(option, shortName))) {
235         ok = true;
236         if (param.empty()) {
237             value = def;
238         } else if (boost::iequals(param, "t") ||
239                    boost::iequals(param, "1") ||
240                    boost::iequals(param, "true") ||
241                    boost::iequals(param, "yes")) {
242             value = true;
243         } else if (boost::iequals(param, "f") ||
244               boost::iequals(param, "0") ||
245               boost::iequals(param, "false") ||
246               boost::iequals(param, "no")) {
247             value = false;
248         } else {
249             usage(true, string("parameter in '") + m_argv[opt] + "' must be 1/t/true/yes or 0/f/false/no");
250             ok = false;
251         }
252         // was our option
253         return true;
254     } else {
255         // keep searching for match
256         return false;
257     }
258 }
259
260 bool Cmdline::isSync()
261 {
262     //make sure command line arguments really try to run sync
263     if(m_usage || m_version) {
264         return false;
265     } else if(m_printServers || boost::trim_copy(m_server) == "?")  {
266         return false;
267     } else if(m_printTemplates || m_dontrun) {
268         return false;
269     } else if(m_argc == 1 || (m_useDaemon.wasSet() && m_argc == 2)) {
270         return false;
271     } else if(m_printConfig || m_remove) {
272         return false;
273     } else if (m_server == "" && m_argc > 1) {
274         return false;
275     } else if(m_configure || m_migrate) {
276         return false;
277     } else if(m_status || m_printSessions) {
278         return false;
279     } else if(!m_restore.empty()) {
280         return false;
281     } else if(m_dryrun) {
282         return false;
283     } else if(!m_run && (m_syncProps.size() || m_sourceProps.size())) {
284         return false;
285     }
286     return true;
287 }
288
289 bool Cmdline::dontRun() const
290 {
291     // this mimics the if() checks in run()
292     if (m_usage || m_version ||
293         m_printServers || boost::trim_copy(m_server) == "?" ||
294         m_printTemplates) {
295         return false;
296     } else {
297         return m_dontrun;
298     }
299 }
300
301 bool Cmdline::run() {
302     // --dry-run is only supported by some operations.
303     // Be very strict about it and make sure it is off in all
304     // potentially harmful operations, otherwise users might
305     // expect it to have an effect when it doesn't.
306
307     if (m_usage) {
308         usage(true);
309     } else if (m_version) {
310         printf("SyncEvolution %s\n", VERSION);
311         printf("%s", EDSAbiWrapperInfo());
312         printf("%s", SyncSource::backendsInfo().c_str());
313     } else if (m_printServers || boost::trim_copy(m_server) == "?") {
314         dumpConfigs("Configured servers:",
315                     SyncConfig::getConfigs());
316     } else if (m_printTemplates) {
317         SyncConfig::DeviceList devices;
318         if (m_template.empty()){
319             dumpConfigTemplates("Available configuration templates:",
320                     SyncConfig::getPeerTemplates(devices), false);
321         } else {
322             //limiting at templates for syncml clients only.
323             devices.push_back (SyncConfig::DeviceDescription("", m_template, SyncConfig::MATCH_FOR_SERVER_MODE));
324             dumpConfigTemplates("Available configuration templates:",
325                     SyncConfig::matchPeerTemplates(devices), true);
326         }
327     } else if (m_dontrun) {
328         // user asked for information
329     } else if (m_argc == 1 || (m_useDaemon.wasSet() && m_argc == 2)) {
330         // no parameters: list databases and short usage
331         const SourceRegistry &registry(SyncSource::getSourceRegistry());
332         boost::shared_ptr<FilterConfigNode> sharedNode(new VolatileConfigNode());
333         boost::shared_ptr<FilterConfigNode> configNode(new VolatileConfigNode());
334         boost::shared_ptr<FilterConfigNode> hiddenNode(new VolatileConfigNode());
335         boost::shared_ptr<FilterConfigNode> trackingNode(new VolatileConfigNode());
336         boost::shared_ptr<FilterConfigNode> serverNode(new VolatileConfigNode());
337         SyncSourceNodes nodes(true, sharedNode, configNode, hiddenNode, trackingNode, serverNode, "");
338         SyncSourceParams params("list", nodes);
339         
340         BOOST_FOREACH(const RegisterSyncSource *source, registry) {
341             BOOST_FOREACH(const Values::value_type &alias, source->m_typeValues) {
342                 if (!alias.empty() && source->m_enabled) {
343                     configNode->setProperty("type", *alias.begin());
344                     auto_ptr<SyncSource> source(SyncSource::createSource(params, false));
345                     if (source.get() != NULL) {
346                         listSources(*source, boost::join(alias, " = "));
347                         m_out << "\n";
348                     }
349                 }
350             }
351         }
352
353         usage(false);
354     } else if (m_printConfig) {
355         boost::shared_ptr<SyncConfig> config;
356         ConfigProps syncFilter;
357         SourceFilters_t sourceFilters;
358
359         if (m_template.empty()) {
360             if (m_server.empty()) {
361                 m_err << "ERROR: --print-config requires either a --template or a server name." << endl;
362                 return false;
363             }
364             config.reset(new SyncConfig(m_server));
365             if (!config->exists()) {
366                 m_err << "ERROR: server '" << m_server << "' has not been configured yet." << endl;
367                 return false;
368             }
369
370             syncFilter = m_syncProps;
371             sourceFilters[""] = m_sourceProps;
372         } else {
373             string peer, context;
374             SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(m_template), peer, context);
375
376             config = SyncConfig::createPeerTemplate(peer);
377             if (!config.get()) {
378                 m_err << "ERROR: no configuration template for '" << m_template << "' available." << endl;
379                 return false;
380             }
381
382             getFilters(context, syncFilter, sourceFilters);
383         }
384
385         // determine whether we dump a peer or a context
386         int flags = DUMP_PROPS_NORMAL;
387         string peer, context;
388         SyncConfig::splitConfigString(config->getConfigName(), peer, context);
389         if (peer.empty()) {
390             flags |= HIDE_PER_PEER;
391             checkForPeerProps();
392         } 
393
394         if (m_sources.empty() ||
395             m_sources.find("main") != m_sources.end()) {
396             boost::shared_ptr<FilterConfigNode> syncProps(config->getProperties());
397             syncProps->setFilter(syncFilter);
398             dumpProperties(*syncProps, config->getRegistry(), flags);
399         }
400
401         list<string> sources = config->getSyncSources();
402         sources.sort();
403         BOOST_FOREACH(const string &name, sources) {
404             if (m_sources.empty() ||
405                 m_sources.find(name) != m_sources.end()) {
406                 m_out << endl << "[" << name << "]" << endl;
407                 SyncSourceNodes nodes = config->getSyncSourceNodes(name);
408                 boost::shared_ptr<FilterConfigNode> sourceProps = nodes.getProperties();
409                 SourceFilters_t::const_iterator it = sourceFilters.find(name);
410                 if (it != sourceFilters.end()) {
411                     sourceProps->setFilter(it->second);
412                 } else {
413                     sourceProps->setFilter(sourceFilters[""]);
414                 }
415                 dumpProperties(*sourceProps, SyncSourceConfig::getRegistry(),
416                                flags | ((name != *(--sources.end())) ? HIDE_LEGEND : DUMP_PROPS_NORMAL));
417             }
418         }
419     } else if (m_server == "" && m_argc > 1) {
420         // Options given, but no server - not sure what the user wanted?!
421         usage(true, "server name missing");
422         return false;
423     } else if (m_configure || m_migrate) {
424         if (m_dryrun) {
425             SyncContext::throwError("--dry-run not supported for configuration changes");
426         }
427         if (m_keyring) {
428 #ifndef USE_GNOME_KEYRING
429             m_err << "Error: this syncevolution binary was compiled without support for storing "
430                      "passwords in a keyring. Either store passwords in your configuration "
431                      "files or enter them interactively on each program run." << endl;
432             return false;
433 #endif
434         }
435
436         bool fromScratch = false;
437         string peer, context;
438         SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(m_server), peer, context);
439         if (peer.empty()) {
440             checkForPeerProps();
441         }
442
443         // Both config changes and migration are implemented as copying from
444         // another config (template resp. old one). Migration also moves
445         // the old config.
446         boost::shared_ptr<SyncConfig> from;
447         if (m_migrate) {
448             string oldContext = context;
449             from.reset(new SyncConfig(m_server));
450             if (!from->exists()) {
451                 // for migration into a different context, search for config without context
452                 oldContext = "";
453                 from.reset(new SyncConfig(peer));
454                 if (!from->exists()) {
455                     m_err << "ERROR: server '" << m_server << "' has not been configured yet." << endl;
456                     return false;
457                 }
458             }
459
460             int counter = 0;
461             string oldRoot = from->getRootPath();
462             string suffix;
463             while (true) {
464                 string newname;
465                 ostringstream newsuffix;
466                 newsuffix << ".old";
467                 if (counter) {
468                     newsuffix << "." << counter;
469                 }
470                 suffix = newsuffix.str();
471                 newname = oldRoot + suffix;
472                 if (!rename(oldRoot.c_str(),
473                             newname.c_str())) {
474                     break;
475                 } else if (errno != EEXIST && errno != ENOTEMPTY) {
476                     m_err << "ERROR: renaming " << oldRoot << " to " <<
477                         newname << ": " << strerror(errno) << endl;
478                     return false;
479                 }
480                 counter++;
481             }
482
483             from.reset(new SyncConfig(peer + suffix +
484                                       (oldContext.empty() ? "" : "@") +
485                                       oldContext));
486         } else {
487             from.reset(new SyncConfig(m_server));
488             if (!from->exists()) {
489                 // creating from scratch, look for template
490                 fromScratch = true;
491                 string configTemplate;
492                 if (m_template.empty()) {
493                     // template is the peer name
494                     configTemplate = m_server;
495                     SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(configTemplate), peer, context);
496                 } else {
497                     // Template is specified explicitly. It must not contain a context,
498                     // because the context comes from the config name.
499                     configTemplate = m_template;
500                     if (SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(configTemplate), peer, context)) {
501                         m_err << "ERROR: template " << configTemplate << " must not specify a context." << endl;
502                         return false;
503                     }
504                     string tmp;
505                     SyncConfig::splitConfigString(SyncConfig::normalizeConfigString(m_server), tmp, context);
506                 }
507                 from = SyncConfig::createPeerTemplate(peer);
508                 if (!from.get()) {
509                     m_err << "ERROR: no configuration template for '" << configTemplate << "' available." << endl;
510                     dumpConfigTemplates("Available configuration templates:",
511                                 SyncConfig::getPeerTemplates(SyncConfig::DeviceList()));
512                     return false;
513                 }
514
515                 if (!from->getPeerIsClient()) {
516                     // Templates no longer contain these strings, because
517                     // GUIs would have to localize them. For configs created
518                     // via the command line, the extra hint that these
519                     // properties need to be set is useful, so set these
520                     // strings here. They'll get copied into the new
521                     // config only if no other value was given on the
522                     // command line.
523                     if (!from->getUsername()[0]) {
524                         from->setUsername("your SyncML server account name");
525                     }
526                     if (!from->getPassword()[0]) {
527                         from->setPassword("your SyncML server password");
528                     }
529                 } else {
530                     // uncomment SyncURL, so that it can be shown by
531                     // sync-ui
532                     if (from->getSyncURL().size() == 0) {
533                         from->setSyncURL ("input your peer address here");
534                     }
535                 }
536             }
537         }
538
539         // Apply config changes on-the-fly. Regardless what we do
540         // (changing an existing config, migrating, creating from
541         // a template), existing shared properties in the desired
542         // context must be preserved unless explicitly overwritten.
543         // Therefore read those, update with command line properties,
544         // then set as filter.
545         ConfigProps syncFilter;
546         SourceFilters_t sourceFilters;
547         getFilters(context, syncFilter, sourceFilters);
548         from->setConfigFilter(true, "", syncFilter);
549         BOOST_FOREACH(const SourceFilters_t::value_type &entry, sourceFilters) {
550             from->setConfigFilter(false, entry.first, entry.second);
551         }
552
553         // write into the requested configuration, creating it if necessary
554         boost::shared_ptr<SyncContext> to(createSyncClient());
555         to->copy(*from, !fromScratch && !m_sources.empty() ? &m_sources : NULL);
556
557         // Sources are active now according to the server default.
558         // Disable all sources not selected by user (if any selected)
559         // and those which have no database.
560         if (fromScratch) {
561             list<string> configuredSources = to->getSyncSources();
562             set<string> sources = m_sources;
563             
564             BOOST_FOREACH(const string &source, configuredSources) {
565                 boost::shared_ptr<PersistentSyncSourceConfig> sourceConfig(to->getSyncSourceConfig(source));
566                 string disable = "";
567                 set<string>::iterator entry = sources.find(source);
568                 bool selected = entry != sources.end();
569
570                 if (!m_sources.empty() &&
571                     !selected) {
572                     disable = "not selected";
573                 } else {
574                     if (entry != sources.end()) {
575                         // The command line parameter matched a valid source.
576                         // All entries left afterwards must have been typos.
577                         sources.erase(entry);
578                     }
579
580                     // check whether the sync source works
581                     SyncSourceParams params("list", to->getSyncSourceNodes(source));
582                     auto_ptr<SyncSource> syncSource(SyncSource::createSource(params, false, to.get()));
583                     if (syncSource.get() == NULL) {
584                         disable = "no backend available";
585                     } else {
586                         try {
587                             SyncSource::Databases databases = syncSource->getDatabases();
588                             if (databases.empty()) {
589                                 disable = "no database to synchronize";
590                             }
591                         } catch (...) {
592                             disable = "backend failed";
593                         }
594                     }
595                 }
596
597                 if (!disable.empty()) {
598                     // abort if the user explicitly asked for the sync source
599                     // and it cannot be enabled, otherwise disable it silently
600                     if (selected) {
601                         SyncContext::throwError(source + ": " + disable);
602                     }
603                     sourceConfig->setSync("disabled");
604                 } else if (selected) {
605                     // user absolutely wants it: enable even if off by default
606                     FilterConfigNode::ConfigFilter::const_iterator sync =
607                         m_sourceProps.find(SyncSourceConfig::m_sourcePropSync.getName());
608                     sourceConfig->setSync(sync == m_sourceProps.end() ? "two-way" : sync->second);
609                 }
610             }
611
612             if (!sources.empty()) {
613                 SyncContext::throwError(string("no such source(s): ") + boost::join(sources, " "));
614             }
615         }
616         // give a change to do something before flushing configs to files
617         to->preFlush(*to);
618
619         // done, now write it
620         to->flush();
621
622         // also copy .synthesis dir?
623         if (m_migrate) {
624             string fromDir, toDir;
625             fromDir = from->getRootPath() + "/.synthesis";
626             toDir = to->getRootPath() + "/.synthesis";
627             if (isDir(fromDir)) {
628                 cp_r(fromDir, toDir);
629             }
630         }
631     } else if (m_remove) {
632         if (m_dryrun) {
633             SyncContext::throwError("--dry-run not supported for removing configurations");
634         }
635
636         // extra sanity check
637         if (!m_sources.empty() ||
638             !m_syncProps.empty() ||
639             !m_sourceProps.empty()) {
640             usage(true, "too many parameters for --remove");
641             return false;
642         } else {
643             boost::shared_ptr<SyncConfig> config;
644             config.reset(new SyncConfig(m_server));
645             if (!config->exists()) {
646                 SyncContext::throwError(string("no such configuration: ") + m_server);
647             }
648             config->remove();
649             return true;
650         }
651     } else {
652         std::set<std::string> unmatchedSources;
653         boost::shared_ptr<SyncContext> context;
654         context.reset(createSyncClient());
655         context->setQuiet(m_quiet);
656         context->setDryRun(m_dryrun);
657         context->setConfigFilter(true, "", m_syncProps);
658         context->setOutput(&m_out);
659         if (m_sources.empty()) {
660             if (m_sourceProps.empty()) {
661                 // empty source list, empty source filter => run with
662                 // existing configuration without filtering it
663             } else {
664                 // Special semantic of 'no source selected': apply
665                 // filter only to sources which are
666                 // *active*. Configuration of inactive sources is left
667                 // unchanged. This way we don't activate sync sources
668                 // accidentally when the sync mode is modified
669                 // temporarily.
670                 BOOST_FOREACH(const std::string &source,
671                               context->getSyncSources()) {
672                     boost::shared_ptr<PersistentSyncSourceConfig> source_config =
673                         context->getSyncSourceConfig(source);
674                     if (strcmp(source_config->getSync(), "disabled")) {
675                         context->setConfigFilter(false, source, m_sourceProps);
676                     }
677                 }
678             }
679         } else {
680             // apply (possibly empty) source filter to selected sources
681             BOOST_FOREACH(const std::string &source,
682                           m_sources) {
683                 boost::shared_ptr<PersistentSyncSourceConfig> source_config =
684                         context->getSyncSourceConfig(source);
685                 if (!source_config || !source_config->exists()) {
686                     // invalid source name in m_sources, remember and
687                     // report this below
688                     unmatchedSources.insert(source);
689                 } else if (m_sourceProps.find(SyncSourceConfig::m_sourcePropSync.getName()) ==
690                            m_sourceProps.end()) {
691                     // Sync mode is not set, must override the
692                     // "sync=disabled" set below with the original
693                     // sync mode for the source or (if that is also
694                     // "disabled") with "two-way". The latter is part
695                     // of the command line semantic that listing a
696                     // source activates it.
697                     FilterConfigNode::ConfigFilter filter = m_sourceProps;
698                     string sync = source_config->getSync();
699                     filter[SyncSourceConfig::m_sourcePropSync.getName()] =
700                         sync == "disabled" ? "two-way" : sync;
701                     context->setConfigFilter(false, source, filter);
702                 } else {
703                     // sync mode is set, can use m_sourceProps
704                     // directly to apply it
705                     context->setConfigFilter(false, source, m_sourceProps);
706                 }
707             }
708
709             // temporarily disable the rest
710             FilterConfigNode::ConfigFilter disabled;
711             disabled[SyncSourceConfig::m_sourcePropSync.getName()] = "disabled";
712             context->setConfigFilter(false, "", disabled);
713         }
714
715         // check whether there were any sources specified which do not exist
716         if (unmatchedSources.size()) {
717             context->throwError(string("no such source(s): ") + boost::join(unmatchedSources, " "));
718         }
719
720         if (m_status) {
721             context->status();
722         } else if (m_printSessions) {
723             vector<string> dirs;
724             context->getSessions(dirs);
725             bool first = true;
726             BOOST_FOREACH(const string &dir, dirs) {
727                 if (first) {
728                     first = false;
729                 } else if(!m_quiet) {
730                     m_out << endl;
731                 }
732                 m_out << dir << endl;
733                 if (!m_quiet) {
734                     SyncReport report;
735                     context->readSessionInfo(dir, report);
736                     m_out << report;
737                 }
738             }
739         } else if (!m_restore.empty()) {
740             // sanity checks: either --after or --before must be given, sources must be selected
741             if ((!m_after && !m_before) ||
742                 (m_after && m_before)) {
743                 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)");
744                 return false;
745             }
746             if (m_sources.empty()) {
747                 usage(false, "Sources must be selected explicitly for --restore to prevent accidental restore.");
748                 return false;
749             }
750             context->restore(m_restore,
751                              m_after ?
752                              SyncContext::DATABASE_AFTER_SYNC :
753                              SyncContext::DATABASE_BEFORE_SYNC);
754         } else {
755             if (m_dryrun) {
756                 SyncContext::throwError("--dry-run not supported for running a synchronization");
757             }
758
759             // safety catch: if props are given, then --run
760             // is required
761             if (!m_run &&
762                 (m_syncProps.size() || m_sourceProps.size())) {
763                 usage(false, "Properties specified, but neither '--configure' nor '--run' - what did you want?");
764                 return false;
765             }
766
767             return (context->sync() == STATUS_OK);
768         }
769     }
770
771     return true;
772 }
773
774 string Cmdline::cmdOpt(const char *opt, const char *param)
775 {
776     string res = "'";
777     res += opt;
778     if (param) {
779         res += " ";
780         res += param;
781     }
782     res += "'";
783     return res;
784 }
785
786 bool Cmdline::parseProp(const ConfigPropertyRegistry &validProps,
787                                      FilterConfigNode::ConfigFilter &props,
788                                      const char *opt,
789                                      const char *param,
790                                      const char *propname)
791 {
792     if (!param) {
793         usage(true, string("missing parameter for ") + cmdOpt(opt, param));
794         return false;
795     } else if (boost::trim_copy(string(param)) == "?") {
796         m_dontrun = true;
797         if (propname) {
798             return listPropValues(validProps, propname, opt);
799         } else {
800             return listProperties(validProps, opt);
801         }
802     } else {
803         string propstr;
804         string paramstr;
805         if (propname) {
806             propstr = propname;
807             paramstr = param;
808         } else {
809             const char *equal = strchr(param, '=');
810             if (!equal) {
811                 usage(true, string("the '=<value>' part is missing in: ") + cmdOpt(opt, param));
812                 return false;
813             }
814             propstr.assign(param, equal - param);
815             paramstr.assign(equal + 1);
816         }
817
818         boost::trim(propstr);
819         boost::trim_left(paramstr);
820
821         if (boost::trim_copy(paramstr) == "?") {
822             m_dontrun = true;
823             return listPropValues(validProps, propstr, cmdOpt(opt, param));
824         } else {
825             const ConfigProperty *prop = validProps.find(propstr);
826             if (!prop) {
827                 m_err << "ERROR: " << cmdOpt(opt, param) << ": no such property" << endl;
828                 return false;
829             } else {
830                 string error;
831                 if (!prop->checkValue(paramstr, error)) {
832                     m_err << "ERROR: " << cmdOpt(opt, param) << ": " << error << endl;
833                     return false;
834                 } else {
835                     props[propstr] = paramstr;
836                     return true;                        
837                 }
838             }
839         }
840     }
841 }
842
843 bool Cmdline::listPropValues(const ConfigPropertyRegistry &validProps,
844                                           const string &propName,
845                                           const string &opt)
846 {
847     const ConfigProperty *prop = validProps.find(propName);
848     if (!prop) {
849         m_err << "ERROR: "<< opt << ": no such property" << endl;
850         return false;
851     } else {
852         m_out << opt << endl;
853         string comment = prop->getComment();
854
855         if (comment != "") {
856             list<string> commentLines;
857             ConfigProperty::splitComment(comment, commentLines);
858             BOOST_FOREACH(const string &line, commentLines) {
859                 m_out << "   " << line << endl;
860             }
861         } else {
862             m_out << "   no documentation available" << endl;
863         }
864         return true;
865     }
866 }
867
868 bool Cmdline::listProperties(const ConfigPropertyRegistry &validProps,
869                                           const string &opt)
870 {
871     // The first of several related properties has a comment.
872     // Remember that comment and print it as late as possible,
873     // that way related properties preceed their comment.
874     string comment;
875     BOOST_FOREACH(const ConfigProperty *prop, validProps) {
876         if (!prop->isHidden()) {
877             string newComment = prop->getComment();
878
879             if (newComment != "") {
880                 if (!comment.empty()) {
881                     dumpComment(m_out, "   ", comment);
882                     m_out << endl;
883                 }
884                 comment = newComment;
885             }
886             m_out << prop->getName() << ":" << endl;
887         }
888     }
889     dumpComment(m_out, "   ", comment);
890     return true;
891 }
892
893 void Cmdline::getFilters(const string &context,
894                          ConfigProps &syncFilter,
895                          map<string, ConfigProps> &sourceFilters)
896 {
897     // Read from context. If it does not exist, we simply set no properties
898     // as filter. Previously there was a check for existance, but that was
899     // flawed because it ignored the global property "defaultPeer".
900     boost::shared_ptr<SyncConfig> shared(new SyncConfig(string("@") + context));
901     shared->getProperties()->readProperties(syncFilter);
902     BOOST_FOREACH(StringPair entry, m_syncProps) {
903         syncFilter[entry.first] = entry.second;
904     }
905
906     BOOST_FOREACH(std::string source, shared->getSyncSources()) {
907         SyncSourceNodes nodes = shared->getSyncSourceNodes(source, "");
908         ConfigProps &props = sourceFilters[source];
909         nodes.getProperties()->readProperties(props);
910
911         // Special case "type" property: the value in the context
912         // is not preserved. Every new peer must ensure that
913         // its own value is compatible (= same backend) with
914         // the other peers.
915         props.erase("type");
916
917         BOOST_FOREACH(StringPair entry, m_sourceProps) {
918             props[entry.first] = entry.second;
919         }
920     }
921     sourceFilters[""] = m_sourceProps;
922 }
923
924 static void findPeerProps(FilterConfigNode::ConfigFilter &filter,
925                           ConfigPropertyRegistry &registry,
926                           list<string> &peerProps)
927 {
928     BOOST_FOREACH(StringPair entry, filter) {
929         const ConfigProperty *prop = registry.find(entry.first);
930         if (prop &&
931             prop->getSharing() == ConfigProperty::NO_SHARING &&
932             !(prop->getFlags() & ConfigProperty::SHARED_AND_UNSHARED)) {
933             peerProps.push_back(entry.first);
934         }
935     }
936 }
937
938 void Cmdline::checkForPeerProps()
939 {
940     list<string> peerProps;
941
942     findPeerProps(m_syncProps, SyncConfig::getRegistry(), peerProps);
943     findPeerProps(m_sourceProps, SyncSourceConfig::getRegistry(), peerProps);
944     if (!peerProps.empty()) {
945         SyncContext::throwError(string("per-peer (unshared) properties not allowed: ") +
946                                 boost::join(peerProps, ", "));
947     }
948 }
949
950 void Cmdline::listSources(SyncSource &syncSource, const string &header)
951 {
952     m_out << header << ":\n";
953     SyncSource::Databases databases = syncSource.getDatabases();
954
955     BOOST_FOREACH(const SyncSource::Database &database, databases) {
956         m_out << "   " << database.m_name << " (" << database.m_uri << ")";
957         if (database.m_isDefault) {
958             m_out << " <default>";
959         }
960         m_out << endl;
961     }
962 }
963
964 void Cmdline::dumpConfigs(const string &preamble,
965                                        const SyncConfig::ConfigList &servers)
966 {
967     m_out << preamble << endl;
968     BOOST_FOREACH(const SyncConfig::ConfigList::value_type &server,servers) {
969         m_out << "   "  << server.first << " = " << server.second <<endl; 
970     }
971     if (!servers.size()) {
972         m_out << "   none" << endl;
973     }
974 }
975
976 void Cmdline::dumpConfigTemplates(const string &preamble,
977                                        const SyncConfig::TemplateList &templates,
978                                        bool printRank)
979 {
980     m_out << preamble << endl;
981     m_out << "   "  << "template name" << " = " << "template description";
982     if (printRank) {
983         m_out << "    " << "matching score in percent (100% = exact match)";
984     }
985     m_out << endl;
986
987     BOOST_FOREACH(const SyncConfig::TemplateList::value_type server,templates) {
988         m_out << "   "  << server->m_templateId << " = " << server->m_description;
989         if (printRank){
990             m_out << "    " << server->m_rank *20 << "%";
991         }
992         m_out << endl;
993     }
994     if (!templates.size()) {
995         m_out << "   none" << endl;
996     }
997 }
998
999 void Cmdline::dumpProperties(const ConfigNode &configuredProps,
1000                              const ConfigPropertyRegistry &allProps,
1001                              int flags)
1002 {
1003     list<string> perPeer, perContext, global;
1004
1005     BOOST_FOREACH(const ConfigProperty *prop, allProps) {
1006         if (prop->isHidden() ||
1007             ((flags & HIDE_PER_PEER) &&
1008              prop->getSharing() == ConfigProperty::NO_SHARING &&
1009              !(prop->getFlags() & ConfigProperty::SHARED_AND_UNSHARED))) {
1010             continue;
1011         }
1012         if (!m_quiet) {
1013             string comment = prop->getComment();
1014             if (!comment.empty()) {
1015                 m_out << endl;
1016                 dumpComment(m_out, "# ", comment);
1017             }
1018         }
1019         bool isDefault;
1020         prop->getProperty(configuredProps, &isDefault);
1021         if (isDefault) {
1022             m_out << "# ";
1023         }
1024         m_out << prop->getName() << " = " << prop->getProperty(configuredProps) << endl;
1025
1026         list<string> *type = NULL;
1027         switch (prop->getSharing()) {
1028         case ConfigProperty::GLOBAL_SHARING:
1029             type = &global;
1030             break;
1031         case ConfigProperty::SOURCE_SET_SHARING:
1032             type = &perContext;
1033             break;
1034         case ConfigProperty::NO_SHARING:
1035             type = &perPeer;
1036             break;
1037         }
1038         if (type) {
1039             type->push_back(prop->getName());
1040         }
1041     }
1042
1043     if (!m_quiet && !(flags & HIDE_LEGEND)) {
1044         if (!perPeer.empty() ||
1045             !perContext.empty() ||
1046             !global.empty()) {
1047             m_out << endl;
1048         }
1049         if (!perPeer.empty()) {
1050             m_out << "# per-peer (unshared) properties: " << boost::join(perPeer, ", ") << endl;
1051         }
1052         if (!perContext.empty()) {
1053             m_out << "# shared by peers in same context: " << boost::join(perContext, ", ") << endl;
1054         }
1055         if (!global.empty()) {
1056             m_out << "# global properties: " << boost::join(global, ", ") << endl;
1057         }
1058     }
1059 }
1060
1061 void Cmdline::dumpComment(ostream &stream,
1062                                        const string &prefix,
1063                                        const string &comment)
1064 {
1065     list<string> commentLines;
1066     ConfigProperty::splitComment(comment, commentLines);
1067     BOOST_FOREACH(const string &line, commentLines) {
1068         stream << prefix << line << endl;
1069     }
1070 }
1071
1072 void Cmdline::usage(bool full, const string &error, const string &param)
1073 {
1074     ostream &out(error.empty() ? m_out : m_err);
1075
1076     out << "Show available sources:" << endl;
1077     out << "  " << m_argv[0] << endl;
1078     out << "Show information about configuration(s) and sync sessions:" << endl;
1079     out << "  " << m_argv[0] << " --print-servers|--print-configs|--print-peers" << endl;
1080     out << "  " << m_argv[0] << " --print-config [--quiet] <config> [main|<source ...]" << endl;
1081     out << "  " << m_argv[0] << " --print-sessions [--quiet] <config>" << endl;
1082     out << "Show information about SyncEvolution:" << endl;
1083     out << "  " << m_argv[0] << " --help|-h" << endl;
1084     out << "  " << m_argv[0] << " --version" << endl;
1085     out << "Run a synchronization:" << endl;
1086     out << "  " << m_argv[0] << " <config> [<source> ...]" << endl;
1087     out << "  " << m_argv[0] << " --run <options for run> <config> [<source> ...]" << endl;
1088     out << "Restore data from the automatic backups:" << endl;
1089     out << "  " << m_argv[0] << " --restore <session directory> --before|--after [--dry-run] <config> <source> ..." << endl;
1090     out << "Remove a configuration:" << endl;
1091     out << "  " << m_argv[0] << " --remove <config>" << endl;
1092     out << "Modify configuration:" << endl;
1093     out << "  " << m_argv[0] << " --configure <options for configuration> <config> [<source> ...]" << endl;
1094     out << "  " << m_argv[0] << " --migrate <config>" << endl;
1095     if (full) {
1096         out << endl <<
1097             "Options:" << endl <<
1098             "--sync|-s <mode>" << endl <<
1099             "--sync|-s ?" << endl <<
1100             "  Temporarily synchronize the active sources in that mode. Useful" << endl <<
1101             "  for a \"refresh-from-server\" or \"refresh-from-client\" sync which" << endl <<
1102             "  clears all data at one end and copies all items from the other." << endl <<
1103             "" << endl <<
1104             "--print-servers|--print-configs|--print-peers" << endl <<
1105             "  Prints the names of all configured peers to stdout." << endl <<
1106             "" << endl <<
1107             "--print-config|-p" << endl <<
1108             "  Prints the complete configuration for the selected peer" << endl <<
1109             "  to stdout, including up-to-date comments for all properties. The" << endl <<
1110             "  format is the normal .ini format with source configurations in" << endl <<
1111             "  different sections introduced with [<source>] lines. Can be combined" << endl <<
1112             "  with --sync-property and --source-property to modify the configuration" << endl <<
1113             "  on-the-fly. When one or more sources are listed after the <config>" << endl <<
1114             "  name on the command line, then only the configs of those sources are" << endl <<
1115             "  printed. Using --quiet suppresses the comments for each property." << endl <<
1116             "  When setting a --template, then the reference configuration for" << endl <<
1117             "  that peer is printed instead of an existing configuration." << endl <<
1118             "" << endl <<
1119             "--print-sessions" << endl <<
1120             "  Prints a list of all previous log directories. Unless --quiet is used, each" << endl <<
1121             "  file name is followed by the original sync report." << endl <<
1122             "" << endl <<
1123             "--configure|-c" << endl <<
1124             "  Modify the configuration files for the selected peer. If no such" << endl <<
1125             "  configuration exists, then a new one is created using one of the" << endl <<
1126             "  template configurations (see --template option). When creating" << endl <<
1127             "  a new configuration only the active sources will be set to active" << endl <<
1128             "  in the new configuration, i.e. \"syncevolution -c scheduleworld addressbook\"" << endl <<
1129             "  followed by \"syncevolution scheduleworld\" will only synchronize the" << endl <<
1130             "  address book. The other sources are created in a disabled state." << endl <<
1131             "  When modifying an existing configuration and sources are specified," << endl <<
1132             "  then the source properties of only those sources are modified." << endl <<
1133             "" << endl <<
1134             "--migrate" << endl <<
1135             "  In older SyncEvolution releases a different layout of configuration files" << endl <<
1136             "  was used. Using --migrate will automatically migrate to the new" << endl <<
1137             "  layout and rename the <config> into <config>.old to prevent accidental use" << endl <<
1138             "  of the old configuration. WARNING: old SyncEvolution releases cannot" << endl <<
1139             "  use the new configuration!" << endl <<
1140             "" << endl <<
1141             "  The switch can also be used to migrate a configuration in the current" << endl <<
1142             "  configuration directory: this preserves all property values, discards" << endl <<
1143             "  obsolete properties and sets all comments exactly as if the configuration" << endl <<
1144             "  had been created from scratch. WARNING: custom comments in the" << endl <<
1145             "  configuration are not preserved." << endl <<
1146             "" << endl <<
1147             "--restore" << endl <<
1148             "  Restores the data of the selected sources to the state from before or after the" << endl <<
1149             "  selected synchronization. The synchronization is selected via its log directory" << endl <<
1150             "  (see --print-sessions). Other directories can also be given as long as" << endl <<
1151             "  they contain database dumps in the format created by SyncEvolution." << endl <<
1152             "  The output includes information about the changes made during the" << endl <<
1153             "  restore, both in terms of item changes and content changes (which is" << endl <<
1154             "  not always the same, see manual for details). This output can be suppressed" << endl <<
1155             "  with --quiet." << endl <<
1156             "  In combination with --dry-run, the changes to local data are only simulated." << endl <<
1157             "  This can be used to check that --restore will not remove valuable information." << endl <<
1158             "" << endl <<
1159             "--remove" << endl <<
1160             "  Deletes the configuration. If the <config> refers to a specific" << endl <<
1161             "  peer, only that peer's configuration is removed. If it refers to" << endl <<
1162             "  a context, that context and all peers inside it are removed." << endl <<
1163             "  Note that there is no confirmation question. Neither local data" << endl <<
1164             "  referenced by the configuration nor the content of log dirs are" << endl <<
1165             "  deleted." << endl <<
1166             "" << endl <<
1167             "--sync-property|-y <property>=<value>" << endl <<
1168             "--sync-property|-y ?" << endl <<
1169             "--sync-property|-y <property>=?" << endl <<
1170             "  Overrides a source-independent configuration property for the" << endl <<
1171             "  current synchronization run or permanently when --configure is used" << endl <<
1172             "  to update the configuration. Can be used multiple times.  Specifying" << endl <<
1173             "  an unused property will trigger an error message." << endl <<
1174             "" << endl <<
1175             "  When using the configuration layout introduced with 1.0, some of the" << endl <<
1176             "  sync properties are shared between peers, for example the directory" << endl <<
1177             "  where sessions are logged. Permanently changing such a shared" << endl <<
1178             "  property for one peer will automatically update the property for all" << endl <<
1179             "  other peers in the same context because the property is stored in a" << endl <<
1180             "  shared config file." << endl <<
1181             "" << endl <<
1182             "--source-property|-z <property>=<value>" << endl <<
1183             "--source-property|-z ?" << endl <<
1184             "--source-property|-z <property>=?" << endl <<
1185             "  Same as --sync-property, but applies to the configuration of all active" << endl <<
1186             "  sources. \"--sync <mode>\" is a shortcut for \"--source-property sync=<mode>\"." << endl <<
1187             "" << endl <<
1188             "--template|-l <peer name>|default|?|?<device>" << endl <<
1189             "  Can be used to select from one of the built-in default configurations" << endl <<
1190             "  for known SyncML peers. Defaults to the <config> name, so --template" << endl <<
1191             "  only has to be specified when creating multiple different configurations" << endl <<
1192             "  for the same peer, or when using a template that is named differently" << endl <<
1193             "  than the peer. \"default\" is an alias for \"scheduleworld\" and can be" << endl <<
1194             "  used as the starting point for servers which do not have a built-in" << endl <<
1195             "  template." << endl <<
1196             "" << endl <<
1197             "  Each template contains a pseudo-random device ID. Therefore setting the" << endl <<
1198             "  \"deviceId\" sync property is only necessary when manually recreating a" << endl <<
1199             "  configuration or when a more descriptive name is desired." << endl <<
1200             "" << endl <<
1201             "  The available templates for different known SyncML servers are listed when" << endl <<
1202             "  using a single question mark instead of template name. When using the" << endl <<
1203             "  ?<device> format, a fuzzy search for a template that might be" << endl <<
1204             "  suitable for talking to such a device is done. The matching works best" << endl <<
1205             "  when using <device> = <Manufacturer>_<Model>. If you don't know the" << endl <<
1206             "  manufacturer, you can just keep it as empty. The output in this mode" << endl <<
1207             "  gives the template name followed by a short description and a rating how well" << endl <<
1208             "  the template matches the device (higher is better)." << endl <<
1209             "" << endl <<
1210             "--status|-t" << endl <<
1211             "  The changes made to local data since the last synchronization are" << endl <<
1212             "  shown without starting a new one. This can be used to see in advance" << endl <<
1213             "  whether the local data needs to be synchronized with the peer." << endl <<
1214             "" << endl <<
1215             "  When used without configuration name, it shows the status of the background" << endl <<
1216             "  sync daemon or an error if no such daemon exists." << endl <<
1217             "" << endl <<
1218             "--quiet|-q" << endl <<
1219             "  Suppresses most of the normal output during a synchronization. The" << endl <<
1220             "  log file still contains all the information." << endl <<
1221             "" << endl <<
1222             "--keyring|-k[=yes/no/...]" << endl <<
1223             "  Save or retrieve passwords from the GNOME keyring when modifying the" << endl <<
1224             "  configuration or running a synchronization. Note that using this option" << endl <<
1225             "  applies to *all* passwords in a configuration, so setting a single" << endl <<
1226             "  password as follows moves the other passwords into the keyring, if" << endl <<
1227             "  they were not stored there already:" << endl <<
1228             "     --keyring --configure --sync-property proxyPassword=foo" << endl <<
1229             "" << endl <<
1230             "  When passwords were stored in the keyring, their value is set to '-'" << endl <<
1231             "  in the configuration. This means that when running a synchronization" << endl <<
1232             "  without the --keyring argument, the password has to be entered" << endl <<
1233             "  interactively. The --print-config output always shows '-' instead of" << endl <<
1234             "  retrieving the password from the keyring." << endl <<
1235             "" << endl <<
1236             "--daemon[=yes/no/...]" << endl <<
1237             "  Run operations in cooperation with the background sync daemon;" << endl <<
1238             "  enabled by default if it is installed." << endl <<
1239             "" << endl <<
1240             "--help|-h" << endl <<
1241             "  Prints usage information." << endl <<
1242             "" << endl <<
1243             "--version" << endl <<
1244             "  Prints the SyncEvolution version." << endl;
1245     }
1246
1247     if (error != "") {
1248         out << endl << "ERROR: " << error << endl;
1249     }
1250     if (param != "") {
1251         out << "INFO: use '" << param << (param[param.size() - 1] == '=' ? "" : " ") <<
1252             "?' to get a list of valid parameters" << endl;
1253     }
1254 }
1255
1256 SyncContext* Cmdline::createSyncClient() {
1257     return new SyncContext(m_server, true);
1258 }
1259
1260 #ifdef ENABLE_UNIT_TESTS
1261
1262 /** simple line-by-line diff */
1263 static string diffStrings(const string &lhs, const string &rhs)
1264 {
1265     ostringstream res;
1266
1267     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
1268     string_split_iterator lit =
1269         boost::make_split_iterator(lhs, boost::first_finder("\n", boost::is_iequal()));
1270     string_split_iterator rit =
1271         boost::make_split_iterator(rhs, boost::first_finder("\n", boost::is_iequal()));
1272     while (lit != string_split_iterator() &&
1273            rit != string_split_iterator()) {
1274         if (*lit != *rit) {
1275             res << "< " << *lit << endl;
1276             res << "> " << *rit << endl;
1277         }
1278         ++lit;
1279         ++rit;
1280     }
1281
1282     while (lit != string_split_iterator()) {
1283         res << "< " << *lit << endl;
1284         ++lit;
1285     }
1286
1287     while (rit != string_split_iterator()) {
1288         res << "> " << *rit << endl;
1289         ++rit;
1290     }
1291
1292     return res.str();
1293 }
1294
1295 # define CPPUNIT_ASSERT_EQUAL_DIFF( expected, actual )      \
1296     do { \
1297         string expected_ = (expected);                                  \
1298         string actual_ = (actual);                                      \
1299         if (expected_ != actual_) {                                     \
1300             CPPUNIT_NS::Message cpputMsg_(string("expected:\n") +       \
1301                                           expected_);                   \
1302             cpputMsg_.addDetail(string("actual:\n") +                   \
1303                                 actual_);                               \
1304             cpputMsg_.addDetail(string("diff:\n") +                     \
1305                                 diffStrings(expected_, actual_));       \
1306             CPPUNIT_NS::Asserter::fail( cpputMsg_,                      \
1307                                         CPPUNIT_SOURCELINE() );         \
1308         } \
1309     } while ( false )
1310
1311 // returns last line, including trailing line break, empty if input is empty
1312 static string lastLine(const string &buffer)
1313 {
1314     if (buffer.size() < 2) {
1315         return buffer;
1316     }
1317
1318     size_t line = buffer.rfind("\n", buffer.size() - 2);
1319     if (line == buffer.npos) {
1320         return buffer;
1321     }
1322
1323     return buffer.substr(line + 1);
1324 }
1325
1326 // true if <word> =
1327 static bool isPropAssignment(const string &buffer) {
1328     size_t start = 0;
1329     while (start < buffer.size() &&
1330            !isspace(buffer[start])) {
1331         start++;
1332     }
1333     if (start + 3 <= buffer.size() &&
1334         buffer.substr(start, 3) == " = ") {
1335         return true;
1336     } else {
1337         return false;
1338     }
1339 }
1340
1341 // remove pure comment lines from buffer,
1342 // also empty lines,
1343 // also defaultPeer (because reference properties do not include global props)
1344 static string filterConfig(const string &buffer)
1345 {
1346     ostringstream res;
1347
1348     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
1349     for (string_split_iterator it =
1350              boost::make_split_iterator(buffer, boost::first_finder("\n", boost::is_iequal()));
1351          it != string_split_iterator();
1352          ++it) {
1353         string line = boost::copy_range<string>(*it);
1354         if (!line.empty() &&
1355             line.find("defaultPeer =") == line.npos &&
1356             (!boost::starts_with(line, "# ") ||
1357              isPropAssignment(line.substr(2)))) {
1358             res << line << endl;
1359         }
1360     }
1361
1362     return res.str();
1363 }
1364
1365 static string injectValues(const string &buffer)
1366 {
1367     string res = buffer;
1368
1369     // username/password not set in templates, only in configs created via
1370     // the command line
1371     boost::replace_first(res,
1372                          "# username = ",
1373                          "username = your SyncML server account name");
1374     boost::replace_first(res,
1375                          "# password = ",
1376                          "password = your SyncML server password");
1377     return res;
1378 }
1379
1380 // remove lines indented with spaces
1381 static string filterIndented(const string &buffer)
1382 {
1383     ostringstream res;
1384     bool first = true;
1385
1386     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
1387     for (string_split_iterator it =
1388              boost::make_split_iterator(buffer, boost::first_finder("\n", boost::is_iequal()));
1389          it != string_split_iterator();
1390          ++it) {
1391         if (!boost::starts_with(*it, " ")) {
1392             if (!first) {
1393                 res << endl;
1394             } else {
1395                 first = false;
1396             }
1397             res << *it;
1398         }
1399     }
1400
1401     return res.str();
1402 }
1403
1404 // sort lines by file, preserving order inside each line
1405 static void sortConfig(string &config)
1406 {
1407     // file name, line number, property
1408     typedef pair<string, pair<int, string> > line_t;
1409     vector<line_t> lines;
1410     typedef boost::split_iterator<string::iterator> string_split_iterator;
1411     int linenr = 0;
1412     for (string_split_iterator it =
1413              boost::make_split_iterator(config, boost::first_finder("\n", boost::is_iequal()));
1414          it != string_split_iterator();
1415          ++it, ++linenr) {
1416         string line(it->begin(), it->end());
1417         if (line.empty()) {
1418             continue;
1419         }
1420
1421         size_t colon = line.find(':');
1422         string prefix = line.substr(0, colon);
1423         lines.push_back(make_pair(prefix, make_pair(linenr, line.substr(colon))));
1424     }
1425
1426     // stable sort because of line number
1427     sort(lines.begin(), lines.end());
1428
1429     size_t len = config.size();
1430     config.resize(0);
1431     config.reserve(len);
1432     BOOST_FOREACH(const line_t &line, lines) {
1433         config += line.first;
1434         config += line.second.second;
1435         config += "\n";
1436     }
1437 }
1438
1439 // convert the internal config dump to .ini style (--print-config)
1440 static string internalToIni(const string &config)
1441 {
1442     ostringstream res;
1443
1444     string section;
1445     typedef boost::split_iterator<string::const_iterator> string_split_iterator;
1446     for (string_split_iterator it =
1447              boost::make_split_iterator(config, boost::first_finder("\n", boost::is_iequal()));
1448          it != string_split_iterator();
1449          ++it) {
1450         string line(it->begin(), it->end());
1451         if (line.empty()) {
1452             continue;
1453         }
1454
1455         size_t colon = line.find(':');
1456         string prefix = line.substr(0, colon);
1457
1458         // internal values are not part of the --print-config output
1459         if (boost::contains(prefix, ".internal.ini") ||
1460             boost::contains(line, "= internal value")) {
1461             continue;
1462         }
1463
1464         // --print-config also doesn't duplicate the "type" property
1465         // => remove the shared property
1466         if (boost::contains(line, ":type = ") &&
1467             boost::starts_with(line, "sources/")) {
1468             continue;
1469         }
1470
1471         // sources/<name>/config.ini or
1472         // spds/sources/<name>/config.ini
1473         size_t endslash = prefix.rfind('/');
1474         if (endslash != line.npos && endslash > 1) {
1475             size_t slash = prefix.rfind('/', endslash - 1);
1476             if (slash != line.npos) {
1477                 string newsource = prefix.substr(slash + 1, endslash - slash - 1);
1478                 if (newsource != section &&
1479                     prefix.find("/sources/") != prefix.npos &&
1480                     newsource != "syncml") {
1481                     res << "[" << newsource << "]" << endl;
1482                     section = newsource;
1483                 }
1484             }
1485         }
1486         string assignment = line.substr(colon + 1);
1487         // substitude aliases with generic values
1488         boost::replace_first(assignment, "= F", "= 0");
1489         boost::replace_first(assignment, "= T", "= 1");
1490         boost::replace_first(assignment, "= syncml:auth-md5", "= md5");
1491         boost::replace_first(assignment, "= syncml:auth-basix", "= basic");
1492         res << assignment << endl;
1493     }
1494
1495     return res.str();
1496 }
1497
1498
1499 /**
1500  * Testing is based on a text representation of a directory
1501  * hierarchy where each line is of the format
1502  * <file path>:<line in file>
1503  *
1504  * The order of files is alphabetical, of lines in the file as
1505  * in the file. Lines in the file without line break cannot
1506  * be represented.
1507  *
1508  * The root of the hierarchy is not part of the representation
1509  * itself.
1510  */
1511 class CmdlineTest : public CppUnit::TestFixture {
1512     CPPUNIT_TEST_SUITE(CmdlineTest);
1513     CPPUNIT_TEST(testFramework);
1514     CPPUNIT_TEST(testSetupScheduleWorld);
1515     CPPUNIT_TEST(testSetupDefault);
1516     CPPUNIT_TEST(testSetupRenamed);
1517     CPPUNIT_TEST(testSetupFunambol);
1518     CPPUNIT_TEST(testSetupSynthesis);
1519     CPPUNIT_TEST(testPrintServers);
1520     CPPUNIT_TEST(testPrintConfig);
1521     CPPUNIT_TEST(testPrintFileTemplates);
1522     CPPUNIT_TEST(testTemplate);
1523     CPPUNIT_TEST(testMatchTemplate);
1524     CPPUNIT_TEST(testAddSource);
1525     CPPUNIT_TEST(testSync);
1526     CPPUNIT_TEST(testConfigure);
1527     CPPUNIT_TEST(testOldConfigure);
1528     CPPUNIT_TEST(testListSources);
1529     CPPUNIT_TEST(testMigrate);
1530     CPPUNIT_TEST_SUITE_END();
1531     
1532 public:
1533     CmdlineTest() :
1534         m_testDir("CmdlineTest"),
1535         // properties sorted by the order in which they are defined
1536         // in the sync and sync source property registry
1537         m_scheduleWorldConfig("peers/scheduleworld/.internal.ini:# HashCode = 0\n"
1538                               "peers/scheduleworld/.internal.ini:# ConfigDate = \n"
1539                               "peers/scheduleworld/.internal.ini:# lastNonce = \n"
1540                               "peers/scheduleworld/.internal.ini:# deviceData = \n"
1541                               "peers/scheduleworld/config.ini:syncURL = http://sync.scheduleworld.com/funambol/ds\n"
1542                               "peers/scheduleworld/config.ini:username = your SyncML server account name\n"
1543                               "peers/scheduleworld/config.ini:password = your SyncML server password\n"
1544                               "config.ini:# logdir = \n"
1545                               "peers/scheduleworld/config.ini:# loglevel = 0\n"
1546                               "peers/scheduleworld/config.ini:# printChanges = 1\n"
1547                               "config.ini:# maxlogdirs = 10\n"
1548                               "peers/scheduleworld/config.ini:# autoSync = 0\n"
1549                               "peers/scheduleworld/config.ini:# autoSyncInterval = 30M\n"
1550                               "peers/scheduleworld/config.ini:# autoSyncDelay = 5M\n"
1551                               "peers/scheduleworld/config.ini:# preventSlowSync = 1\n"
1552                               "peers/scheduleworld/config.ini:# useProxy = 0\n"
1553                               "peers/scheduleworld/config.ini:# proxyHost = \n"
1554                               "peers/scheduleworld/config.ini:# proxyUsername = \n"
1555                               "peers/scheduleworld/config.ini:# proxyPassword = \n"
1556                               "peers/scheduleworld/config.ini:# clientAuthType = md5\n"
1557                               "peers/scheduleworld/config.ini:# RetryDuration = 5M\n"
1558                               "peers/scheduleworld/config.ini:# RetryInterval = 2M\n"
1559                               "peers/scheduleworld/config.ini:# remoteIdentifier = \n"
1560                               "peers/scheduleworld/config.ini:# PeerIsClient = 0\n"
1561                               "peers/scheduleworld/config.ini:# SyncMLVersion = \n"
1562                               "peers/scheduleworld/config.ini:# PeerName = \n"
1563                               "config.ini:deviceId = fixed-devid\n" /* this is not the default! */
1564                               "peers/scheduleworld/config.ini:# remoteDeviceId = \n"
1565                               "peers/scheduleworld/config.ini:# enableWBXML = 1\n"
1566                               "peers/scheduleworld/config.ini:# maxMsgSize = 150000\n"
1567                               "peers/scheduleworld/config.ini:# maxObjSize = 4000000\n"
1568                               "peers/scheduleworld/config.ini:# enableCompression = 0\n"
1569                               "peers/scheduleworld/config.ini:# SSLServerCertificates = \n"
1570                               "peers/scheduleworld/config.ini:# SSLVerifyServer = 1\n"
1571                               "peers/scheduleworld/config.ini:# SSLVerifyHost = 1\n"
1572                               "peers/scheduleworld/config.ini:WebURL = http://www.scheduleworld.com\n"
1573                               "peers/scheduleworld/config.ini:# IconURI = \n"
1574                               "peers/scheduleworld/config.ini:ConsumerReady = 1\n"
1575
1576                               "peers/scheduleworld/sources/addressbook/.internal.ini:# adminData = \n"
1577                               "peers/scheduleworld/sources/addressbook/.internal.ini:# synthesisID = 0\n"
1578                               "peers/scheduleworld/sources/addressbook/config.ini:sync = two-way\n"
1579                               "sources/addressbook/config.ini:type = addressbook:text/vcard\n"
1580                               "peers/scheduleworld/sources/addressbook/config.ini:type = addressbook:text/vcard\n"
1581                               "sources/addressbook/config.ini:# evolutionsource = \n"
1582                               "peers/scheduleworld/sources/addressbook/config.ini:uri = card3\n"
1583                               "sources/addressbook/config.ini:# evolutionuser = \n"
1584                               "sources/addressbook/config.ini:# evolutionpassword = \n"
1585
1586                               "peers/scheduleworld/sources/calendar/.internal.ini:# adminData = \n"
1587                               "peers/scheduleworld/sources/calendar/.internal.ini:# synthesisID = 0\n"
1588                               "peers/scheduleworld/sources/calendar/config.ini:sync = two-way\n"
1589                               "sources/calendar/config.ini:type = calendar\n"
1590                               "peers/scheduleworld/sources/calendar/config.ini:type = calendar\n"
1591                               "sources/calendar/config.ini:# evolutionsource = \n"
1592                               "peers/scheduleworld/sources/calendar/config.ini:uri = cal2\n"
1593                               "sources/calendar/config.ini:# evolutionuser = \n"
1594                               "sources/calendar/config.ini:# evolutionpassword = \n"
1595
1596                               "peers/scheduleworld/sources/memo/.internal.ini:# adminData = \n"
1597                               "peers/scheduleworld/sources/memo/.internal.ini:# synthesisID = 0\n"
1598                               "peers/scheduleworld/sources/memo/config.ini:sync = two-way\n"
1599                               "sources/memo/config.ini:type = memo\n"
1600                               "peers/scheduleworld/sources/memo/config.ini:type = memo\n"
1601                               "sources/memo/config.ini:# evolutionsource = \n"
1602                               "peers/scheduleworld/sources/memo/config.ini:uri = note\n"
1603                               "sources/memo/config.ini:# evolutionuser = \n"
1604                               "sources/memo/config.ini:# evolutionpassword = \n"
1605
1606                               "peers/scheduleworld/sources/todo/.internal.ini:# adminData = \n"
1607                               "peers/scheduleworld/sources/todo/.internal.ini:# synthesisID = 0\n"
1608                               "peers/scheduleworld/sources/todo/config.ini:sync = two-way\n"
1609                               "sources/todo/config.ini:type = todo\n"
1610                               "peers/scheduleworld/sources/todo/config.ini:type = todo\n"
1611                               "sources/todo/config.ini:# evolutionsource = \n"
1612                               "peers/scheduleworld/sources/todo/config.ini:uri = task2\n"
1613                               "sources/todo/config.ini:# evolutionuser = \n"
1614                               "sources/todo/config.ini:# evolutionpassword = ")
1615     {
1616 #ifdef ENABLE_LIBSOUP
1617         // path to SSL certificates has to be set only for libsoup
1618         boost::replace_first(m_scheduleWorldConfig,
1619                              "SSLServerCertificates = ",
1620                              "SSLServerCertificates = /etc/ssl/certs/ca-certificates.crt:/etc/pki/tls/certs/ca-bundle.crt:/usr/share/ssl/certs/ca-bundle.crt");
1621 #endif
1622     }
1623
1624 protected:
1625
1626     /** verify that createFiles/scanFiles themselves work */
1627     void testFramework() {
1628         const string root(m_testDir);
1629         const string content("baz:line\n"
1630                              "caz/subdir:booh\n"
1631                              "caz/subdir2/sub:# comment\n"
1632                              "caz/subdir2/sub:# foo = bar\n"
1633                              "caz/subdir2/sub:# empty = \n"
1634                              "caz/subdir2/sub:# another comment\n"
1635                              "foo:bar1\n"
1636                              "foo:\n"
1637                              "foo: \n"
1638                              "foo:bar2\n");
1639         const string filtered("baz:line\n"
1640                               "caz/subdir:booh\n"
1641                               "caz/subdir2/sub:# foo = bar\n"
1642                               "caz/subdir2/sub:# empty = \n"
1643                               "foo:bar1\n"
1644                               "foo: \n"
1645                               "foo:bar2\n");
1646         createFiles(root, content);
1647         string res = scanFiles(root);
1648         CPPUNIT_ASSERT_EQUAL_DIFF(filtered, res);
1649     }
1650
1651     void removeRandomUUID(string &buffer) {
1652         string uuidstr = "deviceId = syncevolution-";
1653         size_t uuid = buffer.find(uuidstr);
1654         CPPUNIT_ASSERT(uuid != buffer.npos);
1655         size_t end = buffer.find("\n", uuid + uuidstr.size());
1656         CPPUNIT_ASSERT(end != buffer.npos);
1657         buffer.replace(uuid, end - uuid, "deviceId = fixed-devid");
1658     }
1659
1660     /** create new configurations */
1661     void testSetupScheduleWorld() { doSetupScheduleWorld(false); }
1662     void doSetupScheduleWorld(bool shared) {
1663         string root;
1664         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
1665         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1666         ScopedEnvChange home("HOME", m_testDir);
1667
1668         root = m_testDir;
1669         root += "/syncevolution/default";
1670         string peer;
1671         if (shared) {
1672             peer = root + "/peers/scheduleworld";
1673         } else {
1674             peer = root;
1675         }
1676
1677         {
1678             rm_r(peer);
1679             TestCmdline cmdline("--configure",
1680                                 "--sync-property", "proxyHost = proxy",
1681                                 "scheduleworld",
1682                                 "addressbook",
1683                                 NULL);
1684             cmdline.doit();
1685             string res = scanFiles(root);
1686             removeRandomUUID(res);
1687             string expected = ScheduleWorldConfig();
1688             sortConfig(expected);
1689             boost::replace_first(expected,
1690                                  "# proxyHost = ",
1691                                  "proxyHost = proxy");
1692             boost::replace_all(expected,
1693                                "sync = two-way",
1694                                "sync = disabled");
1695             boost::replace_first(expected,
1696                                  "addressbook/config.ini:sync = disabled",
1697                                  "addressbook/config.ini:sync = two-way");
1698             CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
1699         }
1700
1701         {
1702             rm_r(peer);
1703             TestCmdline cmdline("--configure",
1704                                 "--sync-property", "deviceID = fixed-devid",
1705                                 "scheduleworld",
1706                                 NULL);
1707             cmdline.doit();
1708             string res = scanFiles(root);
1709             string expected = ScheduleWorldConfig();
1710             sortConfig(expected);
1711             CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
1712         }
1713     }
1714
1715     void testSetupDefault() {
1716         string root;
1717         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
1718         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1719         ScopedEnvChange home("HOME", m_testDir);
1720
1721         root = m_testDir;
1722         root += "/syncevolution/default";
1723         rm_r(root);
1724         TestCmdline cmdline("--configure",
1725                             "--template", "default",
1726                             "--sync-property", "deviceID = fixed-devid",
1727                             "some-other-server",
1728                             NULL);
1729         cmdline.doit();
1730         string res = scanFiles(root, "some-other-server");
1731         string expected = ScheduleWorldConfig();
1732         sortConfig(expected);
1733         boost::replace_all(expected, "/scheduleworld/", "/some-other-server/");
1734         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
1735     }
1736     void testSetupRenamed() {
1737         string root;
1738         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
1739         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1740         ScopedEnvChange home("HOME", m_testDir);
1741
1742         root = m_testDir;
1743         root += "/syncevolution/default";
1744         rm_r(root);
1745         TestCmdline cmdline("--configure",
1746                             "--template", "scheduleworld",
1747                             "--sync-property", "deviceID = fixed-devid",
1748                             "scheduleworld2",
1749                             NULL);
1750         cmdline.doit();
1751         string res = scanFiles(root, "scheduleworld2");
1752         string expected = ScheduleWorldConfig();
1753         sortConfig(expected);
1754         boost::replace_all(expected, "/scheduleworld/", "/scheduleworld2/");
1755         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
1756     }
1757
1758     void testSetupFunambol() { doSetupFunambol(false); }
1759     void doSetupFunambol(bool shared) {
1760         string root;
1761         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
1762         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1763         ScopedEnvChange home("HOME", m_testDir);
1764
1765         root = m_testDir;
1766         root += "/syncevolution/default";
1767         string peer;
1768         if (shared) {
1769             peer = root + "/peers/funambol";
1770         } else {
1771             peer = root;
1772         }
1773
1774         rm_r(peer);
1775         const char * const argv_fixed[] = {
1776                 "--configure",
1777                 "--sync-property", "deviceID = fixed-devid",
1778                 // templates are case-insensitive
1779                 "FunamBOL",
1780                 NULL
1781         }, * const argv_shared[] = {
1782             "--configure",
1783             "FunamBOL",
1784             NULL
1785         };
1786         TestCmdline cmdline(shared ? argv_shared : argv_fixed);
1787         cmdline.doit();
1788         string res = scanFiles(root, "funambol");
1789         string expected = FunambolConfig();
1790         sortConfig(expected);
1791         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
1792     }
1793
1794     void testSetupSynthesis() { doSetupSynthesis(false); }
1795     void doSetupSynthesis(bool shared) {
1796         string root;
1797         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
1798         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1799         ScopedEnvChange home("HOME", m_testDir);
1800
1801         root = m_testDir;
1802         root += "/syncevolution/default";
1803         string peer;
1804         if (shared) {
1805             peer = root + "/peers/synthesis";
1806         } else {
1807             peer = root;
1808         }
1809         rm_r(peer);
1810         const char * const argv_fixed[] = {
1811                 "--configure",
1812                 "--sync-property", "deviceID = fixed-devid",
1813                 "synthesis",
1814                 NULL
1815         }, * const argv_shared[] = {
1816             "--configure",
1817             "synthesis",
1818             NULL
1819         };
1820         TestCmdline cmdline(shared ? argv_shared : argv_fixed);
1821         cmdline.doit();
1822         string res = scanFiles(root, "synthesis");
1823         string expected = SynthesisConfig();
1824         sortConfig(expected);
1825         CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
1826     }
1827
1828     void testTemplate() {
1829         TestCmdline failure("--template", NULL);
1830         CPPUNIT_ASSERT(!failure.m_cmdline->parse());
1831         CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1832         CPPUNIT_ASSERT_EQUAL(string("ERROR: missing parameter for '--template'\n"), lastLine(failure.m_err.str()));
1833
1834         TestCmdline help("--template", "? ", NULL);
1835         help.doit();
1836         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates:\n"
1837                                   "   template name = template description\n"
1838                                   "   Funambol = http://my.funambol.com\n"
1839                                   "   Google = http://m.google.com/sync\n"
1840                                   "   Goosync = http://www.goosync.com/\n"
1841                                   "   Memotoo = http://www.memotoo.com\n"
1842                                   "   Mobical = http://www.mobical.net\n"
1843                                   "   Oracle = http://www.oracle.com/technology/products/beehive/index.html\n"
1844                                   "   Ovi = http://www.ovi.com\n"
1845                                   "   ScheduleWorld = http://www.scheduleworld.com\n"
1846                                   "   SyncEvolution = http://www.syncevolution.org\n"
1847                                   "   Synthesis = http://www.synthesis.ch\n"
1848                                   "   ZYB = http://www.zyb.com\n",
1849                                   help.m_out.str());
1850         CPPUNIT_ASSERT_EQUAL_DIFF("", help.m_err.str());
1851     }
1852
1853     void testMatchTemplate() {
1854         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "testcases/templates");
1855
1856         TestCmdline help1("--template", "?nokia 7210c", NULL);
1857         help1.doit();
1858         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates:\n"
1859                 "   template name = template description    matching score in percent (100% = exact match)\n"
1860                 "   Nokia 7210c = Template for Nokia S40 series Phone    100%\n"
1861                 "   SyncEvolution Client = SyncEvolution server side template    40%\n",
1862                 help1.m_out.str());
1863         CPPUNIT_ASSERT_EQUAL_DIFF("", help1.m_err.str());
1864         TestCmdline help2("--template", "?nokia", NULL);
1865         help2.doit();
1866         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates:\n"
1867                 "   template name = template description    matching score in percent (100% = exact match)\n"
1868                 "   Nokia 7210c = Template for Nokia S40 series Phone    100%\n"
1869                 "   SyncEvolution Client = SyncEvolution server side template    40%\n",
1870                 help2.m_out.str());
1871         CPPUNIT_ASSERT_EQUAL_DIFF("", help2.m_err.str());
1872         TestCmdline help3("--template", "?7210c", NULL);
1873         help3.doit();
1874         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates:\n"
1875                 "   template name = template description    matching score in percent (100% = exact match)\n"
1876                 "   Nokia 7210c = Template for Nokia S40 series Phone    60%\n"
1877                 "   SyncEvolution Client = SyncEvolution server side template    20%\n",
1878                 help3.m_out.str());
1879         CPPUNIT_ASSERT_EQUAL_DIFF("", help3.m_err.str());
1880         TestCmdline help4("--template", "?syncevolution client", NULL);
1881         help4.doit();
1882         CPPUNIT_ASSERT_EQUAL_DIFF("Available configuration templates:\n"
1883                 "   template name = template description    matching score in percent (100% = exact match)\n"
1884                 "   SyncEvolution Client = SyncEvolution server side template    100%\n"
1885                 "   Nokia 7210c = Template for Nokia S40 series Phone    40%\n",
1886                 help4.m_out.str());
1887         CPPUNIT_ASSERT_EQUAL_DIFF("", help4.m_err.str());
1888     }
1889
1890     void testPrintServers() {
1891         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
1892         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1893         ScopedEnvChange home("HOME", m_testDir);
1894
1895         rm_r(m_testDir);
1896         doSetupScheduleWorld(false);
1897         doSetupSynthesis(true);
1898         doSetupFunambol(true);
1899
1900         TestCmdline cmdline("--print-servers", NULL);
1901         cmdline.doit();
1902         CPPUNIT_ASSERT_EQUAL_DIFF("Configured servers:\n"
1903                                   "   funambol = CmdlineTest/syncevolution/default/peers/funambol\n"
1904                                   "   scheduleworld = CmdlineTest/syncevolution/default/peers/scheduleworld\n"
1905                                   "   synthesis = CmdlineTest/syncevolution/default/peers/synthesis\n",
1906                                   cmdline.m_out.str());
1907         CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1908     }
1909
1910     void testPrintConfig() {
1911         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
1912         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
1913         ScopedEnvChange home("HOME", m_testDir);
1914
1915         rm_r(m_testDir);
1916         testSetupFunambol();
1917
1918         {
1919             TestCmdline failure("--print-config", NULL);
1920             CPPUNIT_ASSERT(failure.m_cmdline->parse());
1921             CPPUNIT_ASSERT(!failure.m_cmdline->run());
1922             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1923             CPPUNIT_ASSERT_EQUAL(string("ERROR: --print-config requires either a --template or a server name.\n"),
1924                                  lastLine(failure.m_err.str()));
1925         }
1926
1927         {
1928             TestCmdline failure("--print-config", "foo", NULL);
1929             CPPUNIT_ASSERT(failure.m_cmdline->parse());
1930             CPPUNIT_ASSERT(!failure.m_cmdline->run());
1931             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1932             CPPUNIT_ASSERT_EQUAL(string("ERROR: server 'foo' has not been configured yet.\n"),
1933                                  lastLine(failure.m_err.str()));
1934         }
1935
1936         {
1937             TestCmdline failure("--print-config", "--template", "foo", NULL);
1938             CPPUNIT_ASSERT(failure.m_cmdline->parse());
1939             CPPUNIT_ASSERT(!failure.m_cmdline->run());
1940             CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
1941             CPPUNIT_ASSERT_EQUAL(string("ERROR: no configuration template for 'foo' available.\n"),
1942                                  lastLine(failure.m_err.str()));
1943         }
1944
1945         {
1946             TestCmdline cmdline("--print-config", "--template", "scheduleworld", NULL);
1947             cmdline.doit();
1948             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1949             string actual = cmdline.m_out.str();
1950             // deviceId must be the one from Funambol
1951             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
1952             string filtered = injectValues(filterConfig(actual));
1953             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(ScheduleWorldConfig())),
1954                                       filtered);
1955             // there should have been comments
1956             CPPUNIT_ASSERT(actual.size() > filtered.size());
1957         }
1958
1959         {
1960             TestCmdline cmdline("--print-config", "--template", "scheduleworld@nosuchcontext", NULL);
1961             cmdline.doit();
1962             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1963             string actual = cmdline.m_out.str();
1964             // deviceId must *not* be the one from Funambol because of the new context
1965             CPPUNIT_ASSERT(!boost::contains(actual, "deviceId = fixed-devid"));
1966         }
1967
1968         {
1969             TestCmdline cmdline("--print-config", "--template", "Default", NULL);
1970             cmdline.doit();
1971             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1972             string actual = injectValues(filterConfig(cmdline.m_out.str()));
1973             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
1974             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(ScheduleWorldConfig())),
1975                                       actual);
1976         }
1977
1978         {
1979             TestCmdline cmdline("--print-config", "funambol", NULL);
1980             cmdline.doit();
1981             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1982             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(FunambolConfig())),
1983                                       injectValues(filterConfig(cmdline.m_out.str())));
1984         }
1985
1986         {
1987             // override context and template properties
1988             TestCmdline cmdline("--print-config", "--template", "scheduleworld",
1989                                 "--sync-property", "syncURL=foo",
1990                                 "--source-property", "evolutionsource=Personal",
1991                                 "--source-property", "sync=disabled",
1992                                 NULL);
1993             cmdline.doit();
1994             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
1995             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
1996             boost::replace_first(expected,
1997                                  "syncURL = http://sync.scheduleworld.com/funambol/ds",
1998                                  "syncURL = foo");
1999             boost::replace_all(expected,
2000                                "# evolutionsource = ",
2001                                "evolutionsource = Personal");
2002             boost::replace_all(expected,
2003                                "sync = two-way",
2004                                "sync = disabled");
2005             string actual = injectValues(filterConfig(cmdline.m_out.str()));
2006             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
2007             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2008                                       actual);
2009         }
2010
2011         {
2012             TestCmdline cmdline("--print-config", "--quiet",
2013                                 "--template", "scheduleworld",
2014                                 "funambol",
2015                                 NULL);
2016             cmdline.doit();
2017             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2018             string actual = cmdline.m_out.str();
2019             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
2020             CPPUNIT_ASSERT_EQUAL_DIFF(internalToIni(ScheduleWorldConfig()),
2021                                       injectValues(filterConfig(actual)));
2022         }
2023
2024         {
2025             // change shared source properties, then check template again
2026             TestCmdline cmdline("--configure",
2027                                 "--source-property", "evolutionsource=Personal",
2028                                 "funambol",
2029                                 NULL);
2030             cmdline.doit();
2031             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2032         }
2033         {
2034             TestCmdline cmdline("--print-config", "--quiet",
2035                                 "--template", "scheduleworld",
2036                                 "funambol",
2037                                 NULL);
2038             cmdline.doit();
2039             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2040             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
2041             // from modified Funambol config
2042             boost::replace_all(expected,
2043                                "# evolutionsource = ",
2044                                "evolutionsource = Personal");
2045             string actual = injectValues(filterConfig(cmdline.m_out.str()));
2046             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
2047             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2048                                       actual);
2049         }
2050
2051         {
2052             // print config => must not use settings from default context
2053             TestCmdline cmdline("--print-config", "--template", "scheduleworld@nosuchcontext", NULL);
2054             cmdline.doit();
2055             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2056             // source settings *not* from modified Funambol config
2057             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
2058             string actual = injectValues(filterConfig(cmdline.m_out.str()));
2059             CPPUNIT_ASSERT(!boost::contains(actual, "deviceId = fixed-devid"));
2060             removeRandomUUID(actual);
2061             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2062                                       actual);
2063         }
2064
2065         {
2066             // create config => again, must not use settings from default context
2067             TestCmdline cmdline("--configure", "--template", "scheduleworld", "other@other", NULL);
2068             cmdline.doit();
2069             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2070         }
2071         {
2072             TestCmdline cmdline("--print-config", "other@other", NULL);
2073             cmdline.doit();
2074             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2075             // source settings *not* from modified Funambol config
2076             string expected = filterConfig(internalToIni(ScheduleWorldConfig()));
2077             string actual = injectValues(filterConfig(cmdline.m_out.str()));
2078             CPPUNIT_ASSERT(!boost::contains(actual, "deviceId = fixed-devid"));
2079             removeRandomUUID(actual);
2080             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2081                                       actual);
2082         }
2083     }
2084
2085     void testPrintFileTemplates() {
2086         // use local copy of templates in build dir (no need to install)
2087         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "./templates");
2088         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2089         ScopedEnvChange home("HOME", m_testDir);
2090
2091         rm_r(m_testDir);
2092         testSetupFunambol();
2093
2094         {
2095             TestCmdline cmdline("--print-config", "--template", "scheduleworld", NULL);
2096             cmdline.doit();
2097             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2098             string actual = cmdline.m_out.str();
2099             // deviceId must be the one from Funambol
2100             CPPUNIT_ASSERT(boost::contains(actual, "deviceId = fixed-devid"));
2101             string filtered = injectValues(filterConfig(actual));
2102             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(ScheduleWorldConfig())),
2103                                       filtered);
2104             // there should have been comments
2105             CPPUNIT_ASSERT(actual.size() > filtered.size());
2106         }
2107
2108         {
2109             TestCmdline cmdline("--print-config", "funambol", NULL);
2110             cmdline.doit();
2111             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2112             CPPUNIT_ASSERT_EQUAL_DIFF(filterConfig(internalToIni(FunambolConfig())),
2113                                       injectValues(filterConfig(cmdline.m_out.str())));
2114         }
2115     }
2116
2117     void testAddSource() {
2118         string root;
2119         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
2120         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2121         ScopedEnvChange home("HOME", m_testDir);
2122
2123         testSetupScheduleWorld();
2124
2125         root = m_testDir;
2126         root += "/syncevolution/default";
2127
2128         {
2129             TestCmdline cmdline("--configure",
2130                                 "--source-property", "uri = dummy",
2131                                 "scheduleworld",
2132                                 "xyz",
2133                                 NULL);
2134             cmdline.doit();
2135             string res = scanFiles(root);
2136             string expected = ScheduleWorldConfig();
2137             expected += "\n"
2138                 "peers/scheduleworld/sources/xyz/.internal.ini:# adminData = \n"
2139                 "peers/scheduleworld/sources/xyz/.internal.ini:# synthesisID = 0\n"
2140                 "peers/scheduleworld/sources/xyz/config.ini:# sync = disabled\n"
2141                 "peers/scheduleworld/sources/xyz/config.ini:# type = select backend\n"
2142                 "peers/scheduleworld/sources/xyz/config.ini:uri = dummy\n"
2143                 "sources/xyz/config.ini:# type = select backend\n"
2144                 "sources/xyz/config.ini:# evolutionsource = \n"
2145                 "sources/xyz/config.ini:# evolutionuser = \n"
2146                 "sources/xyz/config.ini:# evolutionpassword = ";
2147             sortConfig(expected);
2148             CPPUNIT_ASSERT_EQUAL_DIFF(expected, res);
2149         }
2150     }
2151
2152     void testSync() {
2153         TestCmdline failure("--sync", NULL);
2154         CPPUNIT_ASSERT(!failure.m_cmdline->parse());
2155         CPPUNIT_ASSERT_EQUAL_DIFF("", failure.m_out.str());
2156         CPPUNIT_ASSERT_EQUAL(string("ERROR: missing parameter for '--sync'\n"), lastLine(failure.m_err.str()));
2157
2158         TestCmdline failure2("--sync", "foo", NULL);
2159         CPPUNIT_ASSERT(!failure2.m_cmdline->parse());
2160         CPPUNIT_ASSERT_EQUAL_DIFF("", failure2.m_out.str());
2161         CPPUNIT_ASSERT_EQUAL(string("ERROR: '--sync foo': not one of the valid values (two-way, slow, refresh-from-client = refresh-client, refresh-from-server = refresh-server = refresh, one-way-from-client = one-way-client, one-way-from-server = one-way-server = one-way, disabled = none)\n"), lastLine(failure2.m_err.str()));
2162
2163         TestCmdline help("--sync", " ?", NULL);
2164         help.doit();
2165         CPPUNIT_ASSERT_EQUAL_DIFF("--sync\n"
2166                                   "   requests a certain synchronization mode:\n"
2167                                   "     two-way             = only send/receive changes since last sync\n"
2168                                   "     slow                = exchange all items\n"
2169                                   "     refresh-from-client = discard all remote items and replace with\n"
2170                                   "                           the items on the client\n"
2171                                   "     refresh-from-server = discard all local items and replace with\n"
2172                                   "                           the items on the server\n"
2173                                   "     one-way-from-client = transmit changes from client\n"
2174                                   "     one-way-from-server = transmit changes from server\n"
2175                                   "     none (or disabled)  = synchronization disabled\n",
2176                                   help.m_out.str());
2177         CPPUNIT_ASSERT_EQUAL_DIFF("", help.m_err.str());
2178
2179         TestCmdline filter("--sync", "refresh-from-server", NULL);
2180         CPPUNIT_ASSERT(filter.m_cmdline->parse());
2181         CPPUNIT_ASSERT(!filter.m_cmdline->run());
2182         CPPUNIT_ASSERT_EQUAL_DIFF("", filter.m_out.str());
2183         CPPUNIT_ASSERT_EQUAL_DIFF("sync = refresh-from-server",
2184                                   string(filter.m_cmdline->m_sourceProps));
2185         CPPUNIT_ASSERT_EQUAL_DIFF("",
2186                                   string(filter.m_cmdline->m_syncProps));
2187
2188         TestCmdline filter2("--source-property", "sync=refresh", NULL);
2189         CPPUNIT_ASSERT(filter2.m_cmdline->parse());
2190         CPPUNIT_ASSERT(!filter2.m_cmdline->run());
2191         CPPUNIT_ASSERT_EQUAL_DIFF("", filter2.m_out.str());
2192         CPPUNIT_ASSERT_EQUAL_DIFF("sync = refresh",
2193                                   string(filter2.m_cmdline->m_sourceProps));
2194         CPPUNIT_ASSERT_EQUAL_DIFF("",
2195                                   string(filter2.m_cmdline->m_syncProps));
2196     }
2197
2198     void testConfigure() {
2199         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
2200         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2201         ScopedEnvChange home("HOME", m_testDir);
2202
2203         rm_r(m_testDir);
2204         testSetupScheduleWorld();
2205         string expected = doConfigure(ScheduleWorldConfig(), "sources/addressbook/config.ini:");
2206
2207         {
2208             // updating type for peer must also update type for context
2209             TestCmdline cmdline("--configure",
2210                                 "--source-property", "type=file:text/vcard:3.0",
2211                                 "scheduleworld", "addressbook",
2212                                 NULL);
2213             cmdline.doit();
2214             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2215             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2216             boost::replace_all(expected,
2217                                "type = addressbook:text/vcard",
2218                                "type = file:text/vcard:3.0");
2219             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2220                                       filterConfig(printConfig("scheduleworld")));
2221             string shared = filterConfig(printConfig("@default"));
2222             CPPUNIT_ASSERT(shared.find("type = file:text/vcard:3.0") != shared.npos);
2223         }
2224
2225         {
2226             // updating type for context must not affect peer
2227             TestCmdline cmdline("--configure",
2228                                 "--source-property", "type=file:text/vcard:2.1",
2229                                 "@default", "addressbook",
2230                                 NULL);
2231             cmdline.doit();
2232             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2233             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2234             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2235                                       filterConfig(printConfig("scheduleworld")));
2236             string shared = filterConfig(printConfig("@default"));
2237             CPPUNIT_ASSERT(shared.find("type = file:text/vcard:2.1") != shared.npos);
2238         }
2239
2240         string syncProperties("syncURL:\n"
2241                               "\n"
2242                               "username:\n"
2243                               "\n"
2244                               "password:\n"
2245                               "\n"
2246                               "logdir:\n"
2247                               "\n"
2248                               "loglevel:\n"
2249                               "\n"
2250                               "printChanges:\n"
2251                               "\n"
2252                               "maxlogdirs:\n"
2253                               "\n"
2254                               "autoSync:\n"
2255                               "\n"
2256                               "autoSyncInterval:\n"
2257                               "\n"
2258                               "autoSyncDelay:\n"
2259                               "\n"
2260                               "preventSlowSync:\n"
2261                               "\n"
2262                               "useProxy:\n"
2263                               "\n"
2264                               "proxyHost:\n"
2265                               "\n"
2266                               "proxyUsername:\n"
2267                               "\n"
2268                               "proxyPassword:\n"
2269                               "\n"
2270                               "clientAuthType:\n"
2271                               "\n"
2272                               "RetryDuration:\n"
2273                               "\n"
2274                               "RetryInterval:\n"
2275                               "\n"
2276                               "remoteIdentifier:\n"
2277                               "\n"
2278                               "PeerIsClient:\n"
2279                               "\n"
2280                               "SyncMLVersion:\n"
2281                               "\n"
2282                               "PeerName:\n"
2283                               "\n"
2284                               "deviceId:\n"
2285                               "\n"
2286                               "remoteDeviceId:\n"
2287                               "\n"
2288                               "enableWBXML:\n"
2289                               "\n"
2290                               "maxMsgSize:\n"
2291                               "maxObjSize:\n"
2292                               "\n"
2293                               "enableCompression:\n"
2294                               "\n"
2295                               "SSLServerCertificates:\n"
2296                               "\n"
2297                               "SSLVerifyServer:\n"
2298                               "\n"
2299                               "SSLVerifyHost:\n"
2300                               "\n"
2301                               "WebURL:\n"
2302                               "\n"
2303                               "IconURI:\n"
2304                               "\n"
2305                               "ConsumerReady:\n"
2306                               "\n"
2307                               "defaultPeer:\n");
2308         string sourceProperties("sync:\n"
2309                                 "\n"
2310                                 "type:\n"
2311                                 "\n"
2312                                 "evolutionsource:\n"
2313                                 "\n"
2314                                 "uri:\n"
2315                                 "\n"
2316                                 "evolutionuser:\n"
2317                                 "evolutionpassword:\n");
2318
2319         {
2320             TestCmdline cmdline("--sync-property", "?",
2321                                 NULL);
2322             cmdline.doit();
2323             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2324             CPPUNIT_ASSERT_EQUAL_DIFF(syncProperties,
2325                                       filterIndented(cmdline.m_out.str()));
2326         }
2327
2328         {
2329             TestCmdline cmdline("--source-property", "?",
2330                                 NULL);
2331             cmdline.doit();
2332             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2333             CPPUNIT_ASSERT_EQUAL_DIFF(sourceProperties,
2334                                       filterIndented(cmdline.m_out.str()));
2335         }
2336
2337         {
2338             TestCmdline cmdline("--source-property", "?",
2339                                 "--sync-property", "?",
2340                                 NULL);
2341             cmdline.doit();
2342             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2343             CPPUNIT_ASSERT_EQUAL_DIFF(sourceProperties + syncProperties,
2344                                       filterIndented(cmdline.m_out.str()));
2345         }
2346
2347         {
2348             TestCmdline cmdline("--sync-property", "?",
2349                                 "--source-property", "?",
2350                                 NULL);
2351             cmdline.doit();
2352             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2353             CPPUNIT_ASSERT_EQUAL_DIFF(syncProperties + sourceProperties,
2354                                       filterIndented(cmdline.m_out.str()));
2355         }
2356     }
2357
2358     void testOldConfigure() {
2359         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
2360         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2361         ScopedEnvChange home("HOME", m_testDir);
2362
2363         string oldConfig = OldScheduleWorldConfig();
2364         InitList<string> props = InitList<string>("serverNonce") +
2365             "clientNonce" +
2366             "devInfoHash" +
2367             "HashCode" +
2368             "ConfigDate" +
2369             "deviceData" +
2370             "adminData" +
2371             "synthesisID" +
2372             "lastNonce" +
2373             "last";
2374         BOOST_FOREACH(string &prop, props) {
2375             boost::replace_all(oldConfig,
2376                                prop + " = ",
2377                                prop + " = internal value");
2378         }
2379
2380         rm_r(m_testDir);
2381         createFiles(m_testDir + "/.sync4j/evolution/scheduleworld", oldConfig);
2382         doConfigure(oldConfig, "spds/sources/addressbook/config.txt:");
2383     }
2384
2385     string doConfigure(const string &SWConfig, const string &addressbookPrefix) {
2386         string expected;
2387
2388         {
2389             TestCmdline cmdline("--configure",
2390                                 "--source-property", "sync = disabled",
2391                                 "scheduleworld",
2392                                 NULL);
2393             cmdline.doit();
2394             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2395             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2396             expected = filterConfig(internalToIni(SWConfig));
2397             boost::replace_all(expected,
2398                                "sync = two-way",
2399                                "sync = disabled");
2400             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2401                                       filterConfig(printConfig("scheduleworld")));
2402         }
2403
2404         {
2405             TestCmdline cmdline("--configure",
2406                                 "--source-property", "sync = one-way-from-server",
2407                                 "scheduleworld",
2408                                 "addressbook",
2409                                 NULL);
2410             cmdline.doit();
2411             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2412             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2413             expected = SWConfig;
2414             boost::replace_all(expected,
2415                                "sync = two-way",
2416                                "sync = disabled");
2417             boost::replace_first(expected,
2418                                  addressbookPrefix + "sync = disabled",
2419                                  addressbookPrefix + "sync = one-way-from-server");
2420             expected = filterConfig(internalToIni(expected));
2421             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2422                                       filterConfig(printConfig("scheduleworld")));
2423         }
2424
2425         {
2426             TestCmdline cmdline("--configure",
2427                                 "--sync", "two-way",
2428                                 "-z", "evolutionsource=source",
2429                                 "--sync-property", "maxlogdirs=20",
2430                                 "-y", "LOGDIR=logdir",
2431                                 "scheduleworld",
2432                                 NULL);
2433             cmdline.doit();
2434             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2435             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2436             boost::replace_all(expected,
2437                                "sync = one-way-from-server",
2438                                "sync = two-way");
2439             boost::replace_all(expected,
2440                                "sync = disabled",
2441                                "sync = two-way");
2442             boost::replace_all(expected,
2443                                "# evolutionsource = ",
2444                                "evolutionsource = source");
2445             boost::replace_all(expected,
2446                                "# maxlogdirs = 10",
2447                                "maxlogdirs = 20");
2448             boost::replace_all(expected,
2449                                "# logdir = ",
2450                                "logdir = logdir");
2451             CPPUNIT_ASSERT_EQUAL_DIFF(expected,
2452                                       filterConfig(printConfig("scheduleworld")));
2453         }
2454
2455         return expected;
2456     }
2457
2458     void testListSources() {
2459         // pick the varargs constructor; NULL alone is ambiguous
2460         TestCmdline cmdline(NULL, NULL);
2461         cmdline.doit();
2462         CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2463         // exact output varies, do not test
2464     }
2465
2466     void testMigrate() {
2467         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
2468         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2469         ScopedEnvChange home("HOME", m_testDir);
2470
2471         rm_r(m_testDir);
2472         string oldRoot = m_testDir + "/.sync4j/evolution/scheduleworld";
2473         string newRoot = m_testDir + "/syncevolution/default";
2474
2475         string oldConfig = OldScheduleWorldConfig();
2476
2477         {
2478             // migrate old config
2479             createFiles(oldRoot, oldConfig);
2480             string createdConfig = scanFiles(oldRoot);
2481             TestCmdline cmdline("--migrate",
2482                                 "scheduleworld",
2483                                 NULL);
2484             cmdline.doit();
2485             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2486             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2487
2488             string migratedConfig = scanFiles(newRoot);
2489             string expected = ScheduleWorldConfig();
2490             sortConfig(expected);
2491             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
2492             string renamedConfig = scanFiles(oldRoot + ".old");
2493             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
2494         }
2495
2496         {
2497             // rewrite existing config with obsolete properties
2498             // => these properties should get removed
2499             //
2500             // There is one limitation: shared nodes are not rewritten.
2501             // This is acceptable.
2502             createFiles(newRoot + "/peers/scheduleworld",
2503                         "config.ini:# obsolete comment\n"
2504                         "config.ini:obsoleteprop = foo\n",
2505                         true);
2506             string createdConfig = scanFiles(newRoot, "scheduleworld");
2507
2508             TestCmdline cmdline("--migrate",
2509                                 "scheduleworld",
2510                                 NULL);
2511             cmdline.doit();
2512             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2513             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2514
2515             string migratedConfig = scanFiles(newRoot, "scheduleworld");
2516             string expected = ScheduleWorldConfig();
2517             sortConfig(expected);
2518             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
2519             string renamedConfig = scanFiles(newRoot, "scheduleworld.old");
2520             boost::replace_all(createdConfig, "/scheduleworld/", "/scheduleworld.old/");
2521             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
2522         }
2523
2524         {
2525             // migrate old config with changes and .synthesis directory, a second time
2526             createFiles(oldRoot, oldConfig);
2527             createFiles(oldRoot,
2528                         ".synthesis/dummy-file.bfi:dummy = foobar\n"
2529                         "spds/sources/addressbook/changes/config.txt:foo = bar\n"
2530                         "spds/sources/addressbook/changes/config.txt:foo2 = bar2\n",
2531                         true);
2532             string createdConfig = scanFiles(oldRoot);
2533             rm_r(newRoot);
2534             TestCmdline cmdline("--migrate",
2535                                 "scheduleworld",
2536                                 NULL);
2537             cmdline.doit();
2538             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2539             CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2540
2541             string migratedConfig = scanFiles(newRoot);
2542             string expected = m_scheduleWorldConfig;
2543             sortConfig(expected);
2544             boost::replace_first(expected,
2545                                  "peers/scheduleworld/sources/addressbook/config.ini",
2546                                  "peers/scheduleworld/sources/addressbook/.other.ini:foo = bar\n"
2547                                  "peers/scheduleworld/sources/addressbook/.other.ini:foo2 = bar2\n"
2548                                  "peers/scheduleworld/sources/addressbook/config.ini");
2549             boost::replace_first(expected,
2550                                  "peers/scheduleworld/config.ini",
2551                                  "peers/scheduleworld/.synthesis/dummy-file.bfi:dummy = foobar\n"
2552                                  "peers/scheduleworld/config.ini");
2553             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
2554             string renamedConfig = scanFiles(oldRoot + ".old.1");
2555             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
2556         }
2557
2558         {
2559             string otherRoot = m_testDir + "/syncevolution/other";
2560             rm_r(otherRoot);
2561
2562             // migrate old config into non-default context
2563             createFiles(oldRoot, oldConfig);
2564             string createdConfig = scanFiles(oldRoot);
2565             {
2566                 TestCmdline cmdline("--migrate",
2567                                     "scheduleworld@other",
2568                                     NULL);
2569                 cmdline.doit();
2570                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2571                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2572             }
2573
2574             string migratedConfig = scanFiles(otherRoot);
2575             string expected = ScheduleWorldConfig();
2576             sortConfig(expected);
2577             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
2578             string renamedConfig = scanFiles(oldRoot + ".old");
2579             CPPUNIT_ASSERT_EQUAL_DIFF(createdConfig, renamedConfig);
2580
2581             // migrate the migrated config again inside the "other" context
2582             {
2583                 TestCmdline cmdline("--migrate",
2584                                     "scheduleworld@other",
2585                                     NULL);
2586                 cmdline.doit();
2587                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2588                 CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_out.str());
2589             }
2590             migratedConfig = scanFiles(otherRoot, "scheduleworld");
2591             expected = ScheduleWorldConfig();
2592             sortConfig(expected);
2593             CPPUNIT_ASSERT_EQUAL_DIFF(expected, migratedConfig);
2594             renamedConfig = scanFiles(otherRoot, "scheduleworld.old");
2595             boost::replace_all(expected, "/scheduleworld/", "/scheduleworld.old/");
2596             CPPUNIT_ASSERT_EQUAL_DIFF(expected, renamedConfig);
2597         }
2598     }
2599
2600     const string m_testDir;
2601     string m_scheduleWorldConfig;
2602         
2603
2604 private:
2605
2606     /**
2607      * vararg constructor with NULL termination,
2608      * out and error stream into stringstream members
2609      */
2610     class TestCmdline {
2611         void init() {
2612             m_argv.reset(new const char *[m_argvstr.size() + 1]);
2613             m_argv[0] = "client-test";
2614             for (size_t index = 0;
2615                  index < m_argvstr.size();
2616                  ++index) {
2617                 m_argv[index + 1] = m_argvstr[index].c_str();
2618             }
2619
2620             m_cmdline.set(new Cmdline(m_argvstr.size() + 1, m_argv.get(), m_out, m_err), "cmdline");
2621             
2622         }
2623
2624     public:
2625         TestCmdline(const char *arg, ...) {
2626             va_list argList;
2627             va_start (argList, arg);
2628             for (const char *curr = arg;
2629                  curr;
2630                  curr = va_arg(argList, const char *)) {
2631                 m_argvstr.push_back(curr);
2632             }
2633             va_end(argList);
2634             init();
2635         }
2636
2637         TestCmdline(const char * const argv[]) {
2638             for (int i = 0; argv[i]; i++) {
2639                 m_argvstr.push_back(argv[i]);
2640             }
2641             init();
2642         }
2643
2644         void doit() {
2645             bool success;
2646             success = m_cmdline->parse() &&
2647                 m_cmdline->run();
2648             if (m_err.str().size()) {
2649                 m_out << endl << m_err.str();
2650             }
2651             CPPUNIT_ASSERT(success);
2652         }
2653
2654         ostringstream m_out, m_err;
2655         cxxptr<Cmdline> m_cmdline;
2656
2657     private:
2658         vector<string> m_argvstr;
2659         boost::scoped_array<const char *> m_argv;
2660     };
2661
2662     string ScheduleWorldConfig() {
2663         string config = m_scheduleWorldConfig;
2664
2665 #if 0
2666         // Currently we don't have an icon for ScheduleWorld. If we
2667         // had (MB #2062) one, then this code would ensure that the
2668         // reference config also has the right path for it.
2669         const char *templateDir = getenv("SYNCEVOLUTION_TEMPLATE_DIR");
2670         if (!templateDir) {
2671             templateDir = TEMPLATE_DIR;
2672         }
2673
2674
2675         if (isDir(string(templateDir) + "/ScheduleWorld")) {
2676             boost::replace_all(config,
2677                                "# IconURI = ",
2678                                string("IconURI = file://") + templateDir + "/ScheduleWorld/icon.png");
2679         }
2680 #endif
2681         return config;
2682     }
2683
2684     string OldScheduleWorldConfig() {
2685         // old style paths
2686         string oldConfig =
2687             "spds/syncml/config.txt:syncURL = http://sync.scheduleworld.com/funambol/ds\n"
2688             "spds/syncml/config.txt:username = your SyncML server account name\n"
2689             "spds/syncml/config.txt:password = your SyncML server password\n"
2690             "spds/syncml/config.txt:# logdir = \n"
2691             "spds/syncml/config.txt:# loglevel = 0\n"
2692             "spds/syncml/config.txt:# printChanges = 1\n"
2693             "spds/syncml/config.txt:# maxlogdirs = 10\n"
2694             "spds/syncml/config.txt:# autoSync = 0\n"
2695             "spds/syncml/config.txt:# autoSyncInterval = 30M\n"
2696             "spds/syncml/config.txt:# autoSyncDelay = 5M\n"
2697             "spds/syncml/config.txt:# preventSlowSync = 1\n"
2698             "spds/syncml/config.txt:# useProxy = 0\n"
2699             "spds/syncml/config.txt:# proxyHost = \n"
2700             "spds/syncml/config.txt:# proxyUsername = \n"
2701             "spds/syncml/config.txt:# proxyPassword = \n"
2702             "spds/syncml/config.txt:# clientAuthType = md5\n"
2703             "spds/syncml/config.txt:# RetryDuration = 5M\n"
2704             "spds/syncml/config.txt:# RetryInterval = 2M\n"
2705             "spds/syncml/config.txt:# remoteIdentifier = \n"
2706             "spds/syncml/config.txt:# PeerIsClient = 0\n"
2707             "spds/syncml/config.txt:# SyncMLVersion = \n"
2708             "spds/syncml/config.txt:# PeerName = \n"
2709             "spds/syncml/config.txt:deviceId = fixed-devid\n" /* this is not the default! */
2710             "spds/syncml/config.txt:# remoteDeviceId = \n"
2711             "spds/syncml/config.txt:# enableWBXML = 1\n"
2712             "spds/syncml/config.txt:# maxMsgSize = 150000\n"
2713             "spds/syncml/config.txt:# maxObjSize = 4000000\n"
2714             "spds/syncml/config.txt:# enableCompression = 0\n"
2715 #ifdef ENABLE_LIBSOUP
2716             // path to SSL certificates is only set for libsoup
2717             "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"
2718
2719 #else
2720             "spds/syncml/config.txt:# SSLServerCertificates = \n"
2721 #endif
2722             "spds/syncml/config.txt:# SSLVerifyServer = 1\n"
2723             "spds/syncml/config.txt:# SSLVerifyHost = 1\n"
2724             "spds/syncml/config.txt:WebURL = http://www.scheduleworld.com\n"
2725             "spds/syncml/config.txt:# IconURI = \n"
2726             "spds/syncml/config.txt:ConsumerReady = 1\n"
2727             "spds/sources/addressbook/config.txt:sync = two-way\n"
2728             "spds/sources/addressbook/config.txt:type = addressbook:text/vcard\n"
2729             "spds/sources/addressbook/config.txt:# evolutionsource = \n"
2730             "spds/sources/addressbook/config.txt:uri = card3\n"
2731             "spds/sources/addressbook/config.txt:# evolutionuser = \n"
2732             "spds/sources/addressbook/config.txt:# evolutionpassword = \n"
2733             "spds/sources/calendar/config.txt:sync = two-way\n"
2734             "spds/sources/calendar/config.txt:type = calendar\n"
2735             "spds/sources/calendar/config.txt:# evolutionsource = \n"
2736             "spds/sources/calendar/config.txt:uri = cal2\n"
2737             "spds/sources/calendar/config.txt:# evolutionuser = \n"
2738             "spds/sources/calendar/config.txt:# evolutionpassword = \n"
2739             "spds/sources/memo/config.txt:sync = two-way\n"
2740             "spds/sources/memo/config.txt:type = memo\n"
2741             "spds/sources/memo/config.txt:# evolutionsource = \n"
2742             "spds/sources/memo/config.txt:uri = note\n"
2743             "spds/sources/memo/config.txt:# evolutionuser = \n"
2744             "spds/sources/memo/config.txt:# evolutionpassword = \n"
2745             "spds/sources/todo/config.txt:sync = two-way\n"
2746             "spds/sources/todo/config.txt:type = todo\n"
2747             "spds/sources/todo/config.txt:# evolutionsource = \n"
2748             "spds/sources/todo/config.txt:uri = task2\n"
2749             "spds/sources/todo/config.txt:# evolutionuser = \n"
2750             "spds/sources/todo/config.txt:# evolutionpassword = \n";
2751         return oldConfig;
2752     }
2753
2754     string FunambolConfig() {
2755         string config = m_scheduleWorldConfig;
2756         boost::replace_all(config, "/scheduleworld/", "/funambol/");
2757
2758         boost::replace_first(config,
2759                              "syncURL = http://sync.scheduleworld.com/funambol/ds",
2760                              "syncURL = http://my.funambol.com/sync");
2761
2762         boost::replace_first(config,
2763                              "WebURL = http://www.scheduleworld.com",
2764                              "WebURL = http://my.funambol.com");
2765
2766         boost::replace_first(config,
2767                              "# enableWBXML = 1",
2768                              "enableWBXML = 0");
2769
2770         boost::replace_first(config,
2771                              "# RetryInterval = 2M",
2772                              "RetryInterval = 0");
2773
2774         boost::replace_first(config,
2775                              "addressbook/config.ini:uri = card3",
2776                              "addressbook/config.ini:uri = card");
2777         boost::replace_all(config,
2778                            "addressbook/config.ini:type = addressbook:text/vcard",
2779                            "addressbook/config.ini:type = addressbook");
2780
2781         boost::replace_first(config,
2782                              "calendar/config.ini:uri = cal2",
2783                              "calendar/config.ini:uri = event");
2784         boost::replace_all(config,
2785                            "calendar/config.ini:type = calendar",
2786                            "calendar/config.ini:type = calendar:text/calendar!");
2787
2788         boost::replace_first(config,
2789                              "todo/config.ini:uri = task2",
2790                              "todo/config.ini:uri = task");
2791         boost::replace_all(config,
2792                            "todo/config.ini:type = todo",
2793                            "todo/config.ini:type = todo:text/calendar!");
2794
2795         return config;
2796     }
2797
2798     string SynthesisConfig() {
2799         string config = m_scheduleWorldConfig;
2800         boost::replace_all(config, "/scheduleworld/", "/synthesis/");
2801
2802         boost::replace_first(config,
2803                              "syncURL = http://sync.scheduleworld.com/funambol/ds",
2804                              "syncURL = http://www.synthesis.ch/sync");
2805
2806         boost::replace_first(config,
2807                              "WebURL = http://www.scheduleworld.com",
2808                              "WebURL = http://www.synthesis.ch");        
2809
2810         boost::replace_first(config,
2811                              "ConsumerReady = 1",
2812                              "# ConsumerReady = 0");
2813
2814         boost::replace_first(config,
2815                              "addressbook/config.ini:uri = card3",
2816                              "addressbook/config.ini:uri = contacts");
2817         boost::replace_all(config,
2818                            "addressbook/config.ini:type = addressbook:text/vcard",
2819                            "addressbook/config.ini:type = addressbook");
2820
2821         boost::replace_first(config,
2822                              "calendar/config.ini:uri = cal2",
2823                              "calendar/config.ini:uri = events");
2824         boost::replace_first(config,
2825                              "calendar/config.ini:sync = two-way",
2826                              "calendar/config.ini:sync = disabled");
2827
2828         boost::replace_first(config,
2829                              "memo/config.ini:uri = note",
2830                              "memo/config.ini:uri = notes");
2831
2832         boost::replace_first(config,
2833                              "todo/config.ini:uri = task2",
2834                              "todo/config.ini:uri = tasks");
2835         boost::replace_first(config,
2836                              "todo/config.ini:sync = two-way",
2837                              "todo/config.ini:sync = disabled");
2838
2839         return config;
2840     }          
2841
2842     /** create directory hierarchy, overwriting previous content */
2843     void createFiles(const string &root, const string &content, bool append = false) {
2844         if (!append) {
2845             rm_r(root);
2846         }
2847
2848         size_t start = 0;
2849         ofstream out;
2850         string outname;
2851
2852         out.exceptions(ios_base::badbit|ios_base::failbit);
2853         while (start < content.size()) {
2854             size_t delim = content.find(':', start);
2855             size_t end = content.find('\n', start);
2856             if (delim == content.npos ||
2857                 end == content.npos) {
2858                 // invalid content ?!
2859                 break;
2860             }
2861             string newname = content.substr(start, delim - start);
2862             string line = content.substr(delim + 1, end - delim - 1);
2863             if (newname != outname) {
2864                 if (out.is_open()) {
2865                     out.close();
2866                 }
2867                 string fullpath = root + "/" + newname;
2868                 size_t fileoff = fullpath.rfind('/');
2869                 mkdir_p(fullpath.substr(0, fileoff));
2870                 out.open(fullpath.c_str(),
2871                          append ? ios_base::out : (ios_base::out|ios_base::trunc));
2872                 outname = newname;
2873             }
2874             out << line << endl;
2875             start = end + 1;
2876         }
2877     }
2878
2879     /** turn directory hierarchy into string
2880      *
2881      * @param root       root path in file system
2882      * @param peer       if non-empty, then ignore all <root>/peers/<foo> directories
2883      *                   where <foo> != peer
2884      * @param onlyProps  ignore lines which are comments
2885      */
2886     string scanFiles(const string &root, const string &peer = "", bool onlyProps = true) {
2887         ostringstream out;
2888
2889         scanFiles(root, "", peer, out, onlyProps);
2890         return out.str();
2891     }
2892
2893     void scanFiles(const string &root, const string &dir, const string &peer, ostringstream &out, bool onlyProps) {
2894         string newroot = root;
2895         newroot += "/";
2896         newroot += dir;
2897         ReadDir readDir(newroot);
2898         sort(readDir.begin(), readDir.end());
2899
2900         BOOST_FOREACH(const string &entry, readDir) {
2901             if (isDir(newroot + "/" + entry)) {
2902                 if (boost::ends_with(newroot, "/peers") &&
2903                     !peer.empty() &&
2904                     entry != peer) {
2905                     // skip different peer directory
2906                     continue;
2907                 } else {
2908                     scanFiles(root, dir + (dir.empty() ? "" : "/") + entry, peer, out, onlyProps);
2909                 }
2910             } else {
2911                 ifstream in;
2912                 in.exceptions(ios_base::badbit /* failbit must not trigger exception because is set when reaching eof ?! */);
2913                 in.open((newroot + "/" + entry).c_str());
2914                 string line;
2915                 while (!in.eof()) {
2916                     getline(in, line);
2917                     if ((line.size() || !in.eof()) && 
2918                         (!onlyProps ||
2919                          (boost::starts_with(line, "# ") ?
2920                           isPropAssignment(line.substr(2)) :
2921                           !line.empty()))) {
2922                         if (dir.size()) {
2923                             out << dir << "/";
2924                         }
2925                         out << entry << ":";
2926                         out << line << '\n';
2927                     }
2928                 }
2929             }
2930         }
2931     }
2932
2933     string printConfig(const string &server) {
2934         ScopedEnvChange templates("SYNCEVOLUTION_TEMPLATE_DIR", "/dev/null");
2935         ScopedEnvChange xdg("XDG_CONFIG_HOME", m_testDir);
2936         ScopedEnvChange home("HOME", m_testDir);
2937
2938         TestCmdline cmdline("--print-config", server.c_str(), NULL);
2939         cmdline.doit();
2940         CPPUNIT_ASSERT_EQUAL_DIFF("", cmdline.m_err.str());
2941         return cmdline.m_out.str();
2942     }
2943 };
2944
2945 SYNCEVOLUTION_TEST_SUITE_REGISTRATION(CmdlineTest);
2946
2947 #endif // ENABLE_UNIT_TESTS
2948
2949 SE_END_CXX