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