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