Handle the EINTR error when doing a waitpid() in wvmagicloopback.
[wvapps.git] / evolution / wvexcconn.cc
blobb91d798c8af93c176190ebeb061dfb3d27f65196
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * Authors : Patrick Patterson <ppatters@nit.ca>
4 * Peter Colijn <pcolijn@nit.ca>
5 * Scott MacLean <scott@nit.ca>
6 * William Lachance <wlach@nit.ca>
7 * Hubert Figuiere <hub@nit.ca>
9 * Copyright 2003-2004, Net Integration Technologies, Inc.
11 * This program is free software; you can redistribute it and/or
12 * modify it under the terms of version 2 of the GNU General Public
13 * License as published by the Free Software Foundation.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
23 * USA
26 #include "wvexcconn.h"
27 #include "wvunixsocket.h"
28 #include "wvtimeoutstream.h"
29 #include "wvexcconnmanager.h"
30 #include "wvtcp.h"
31 #include "wvlog.h"
32 #include "wvbuf.h"
33 #include "strutils.h"
34 #include "wvbase64.h"
35 #include "wvsslstream.h"
36 #include "wvstringlist.h"
37 #include "wvencoderstream.h"
38 #include "wvautorelease.h"
39 #include "createfolder.h"
40 #include "deletefolder.h"
41 #include "listfolders.h"
42 #include "listusers.h"
43 #include "update.h"
44 #include "delete.h"
45 #include "setacl.h"
46 #include "getacl.h"
47 #include "sync.h"
48 #include "eituniconfkeys.h"
51 static int wvstrcmp(const WvString *l, const WvString *r)
53 return strcmp(*l, *r);
56 WvExcConn::WvExcConn(WvStream *conn, UniConf _config,
57 WvExcConnManager *_conn_manager,
58 ExchangeItStorage *_storage,
59 const int _protocol_version) :
60 WvStreamClone(conn),
61 log(WvString("WvExcConn-%s", (long)this), WvLog::Debug1),
62 sock(NULL),
63 cmd_tag(1),
64 num_retries(0),
65 transaction_complete(false),
66 nosend(false),
67 have_progress_cb(false),
68 progress(0),
69 conn_manager(_conn_manager),
70 storage(_storage),
71 config(_config),
72 state(Authenticate),
73 protocol_version(_protocol_version)
75 #ifndef NOCS
76 uses_continue_select = true;
77 #endif
78 assert(storage);
79 assert(conn_manager);
81 setclosecallback(IWvStreamCallback(conn_manager,
82 &WvExcConnManager::conn_closed));
83 setclone(conn);
85 log("Connecting using protocol version %s\n",
86 protocol_version);
88 alarm(50);
89 // CB almost immediately to authenticate and do initial folder sync
90 // FIXME: is this the best way of doing this?
93 WvExcConn::~WvExcConn()
95 #ifndef NOCS
96 terminate_continue_select();
97 #endif
99 if (!isok())
101 log(WvLog::Error, "Aborted. Error was: %s\n", errstr());
102 if (!transaction_complete && sock)
104 uint32_t failure = 65535;
105 sock->write(&failure, sizeof(uint32_t));
108 else
109 log(WvLog::Info, "Done.\n");
111 WVRELEASE(sock);
114 // Notify our camel provider of the progress we're making
115 void WvExcConn::progress_notify(int percent)
117 if (!have_progress_cb)
118 return;
120 if (progress < 0 || progress > 100)
121 return;
123 if (percent > 0)
124 progress = percent;
126 progress_cb(progress);
129 ErrorType WvExcConn::authenticate()
131 ErrorType result = AuthOK;
132 log("authenticate()\n");
134 WvString username = config[USERNAME_KEY].getme();
135 WvString password = config[PASSWORD_KEY].getme();
136 WvString serverid = config[SERVERID_KEY].getme();
138 // we are compatible with both versions 2 (3.76) and
139 // 3 (3.80) of the exchangeit protocol
140 WvString auth("%s:%s", username, password);
141 WvString authenc = WvBase64Encoder().strflushstr(auth, true);
143 // Prod the server with the Auth Information..
144 // We pick up the answer the first time into execute()
145 print("Version: %s\n", protocol_version);
146 log(">>> (Version: %s)\n", protocol_version);
147 print("Authenticate: %s\n\n", authenc);
148 log(">>> (Authenticate: %s)\n", authenc);
150 if (!isok())
152 if (result == AuthOK)
154 result = ConnError;
156 log("Sorry, cannot even start authentication.\n");
157 return result;
160 consume_newlines();
162 // FIXME: Ideally want something more robust than a timeout
163 WvString line;
164 while(!!(line = blocking_getline(AUTH_TIMEOUT)))
166 log("<< (%s)\n", line);
167 if (line.len() >= 11 && !strncmp(line, "* ERROR 401", 11))
169 result = AuthFailed;
170 seterr("Authentication failed");
171 break;
174 if (line.len() >= 7 && !strncmp(line, "* ERROR", 7))
176 result = ProtocolError;
177 seterr("Response bad: %s", line);
178 break;
181 // actually this is dangerous as I'm not sure if headers are case sensitive
182 // or not. Probably safer to use strnicmp(). See bug 7277
183 if (!strncmp(line, "Server-ID:", 10))
185 WvStringList parts;
186 parts.split(line, " ");
188 if (parts.count() != 2)
190 result = ProtocolError;
191 log("Wow, that's a messed up Server-Id header:\n%s\n",
192 parts.join(" "));
193 seterr("Response bad: invalid Server-Id header");
194 break;
197 parts.popstr();
198 WvString newid = parts.popstr();
200 if (!!serverid && newid != serverid)
202 log("First time connecting to a new server; migrating.\n");
203 config[OLD_SERVERID_KEY].setme(serverid);
204 config[SERVERID_KEY].setme(newid);
208 consume_newlines();
211 drain();
213 if (!isok())
215 if (result == AuthOK)
217 result = ConnError;
219 log("Sorry, cannot continue after authentication.\n");
220 close();
221 return result;
224 conn_manager->save_protocol_version(protocol_version);
225 log("No obvious errors in response (so far)\n");
226 return result;
229 // returns true on success, false on (non-critical) failure
230 bool WvExcConn::migrate_account()
232 log("Detected change in account info. Migrating account information.\n");
234 if (!storage->migrate_account(config[USERNAME_KEY].getme()))
235 return false;
237 if (config[OLD_USERNAME_KEY].exists())
238 config[OLD_USERNAME_KEY].remove();
240 if (config[OLD_SERVERID_KEY].exists())
241 config[OLD_SERVERID_KEY].remove();
243 return true;
246 bool WvExcConn::initial_folder_sync()
248 send_listfolders();
249 if (!apply_pending_responses(false))
250 return false;
252 // Create a toplevel "Calendar", "Contacts", and "Tasks" folders,
253 // if these don't exist already
254 WvString calendar_folder("%s.Calendar", storage->get_owner());
255 if (!config[FOLDERS_KEY][calendar_folder].exists())
256 storage->add_folder(calendar_folder, EXCHANGEIT_CALENDAR_TYPE, true);
258 WvString contacts_folder("%s.Contacts", storage->get_owner());
259 if (!config[FOLDERS_KEY][contacts_folder].exists())
260 storage->add_folder(contacts_folder, EXCHANGEIT_CONTACTS_TYPE, true);
262 WvString tasks_folder("%s.Tasks", storage->get_owner());
263 if (!config[FOLDERS_KEY][tasks_folder].exists())
264 storage->add_folder(tasks_folder, EXCHANGEIT_TASKS_TYPE, true);
266 return true;
269 void WvExcConn::send_folder_commands()
271 cmd_tag = 1;
272 send_createfolders();
273 send_deletefolders();
274 send_listfolders();
277 // Here we send all the commands needed for a full sync of all folders.
278 // We do not parse/apply responses.
279 void WvExcConn::send_sync_commands()
281 // We were disconnected; retry remaining commands
282 CommandHash *commands = conn_manager->get_commands();
283 WvAutorelease<CommandHash> autoreleaser(commands);
285 log("send_sync_commands(): have progress_cb %s\n", have_progress_cb);
287 if (commands->count())
289 CommandHash::Iter i(*commands);
290 int curr_cmd = 1;
291 int total_cmds = commands->count();
293 log("Found %s unfinished commands. Retrying them.\n",
294 total_cmds);
296 for (i.rewind(); i.next(); )
298 send_command(i().data, i().key);
299 progress_notify(50 * curr_cmd++ / total_cmds);
302 // We're good; do a full normal sync
303 else
305 // The progress indications here are pretty cheesy;
306 // is anybody going to care though?
307 cmd_tag = 1;
308 progress_notify(15);
310 send_listusers();
311 send_setacls();
312 send_getacls();
313 progress_notify(30);
315 send_deletes();
316 send_updates();
317 send_syncs();
318 progress_notify(50);
322 // Here we read from the stream, and apply any responses we receive.
323 // (commands read the data they need now, not us)
324 bool WvExcConn::apply_pending_responses(bool show_progress)
326 CommandHash *commands = conn_manager->get_commands();
327 WvAutorelease<CommandHash> autoreleaser(commands);
328 log("apply_pending_responses\n");
330 if (!commands)
332 log("Command hash is gone; unable to apply repsonses\n");
333 return false;
336 int cmd_num;
337 int curr_cmd = 1;
338 int total_cmds = commands->count();
339 TaggedCommand *tagged_command;
340 WvString tag;
341 WvString command;
342 WvString response;
343 WvStringList parts;
345 // We call consume_newlines() at the end of each iteration to
346 // make this loop work.
347 while (!!(response = blocking_getline(TIMEOUT)))
349 log("<< (%s)\n", response);
350 parts.zap();
351 parts.split(response, " ");
352 tag = parts.popstr();
353 tagged_command = NULL;
355 log("Applying response for command %s\n", tag);
357 if (tag == "*")
359 command = parts.popstr();
360 GenericCommand *generic_command =
361 GenericCommand::get_command(command);
363 if (!generic_command)
364 log("Didn't get a command for %s!\n", command);
366 if (!generic_command ||
367 !generic_command->apply_response(parts, *this, storage))
368 log("Applying generic command %s failed!\n", command);
370 delete generic_command;
372 else
374 cmd_num = tag.num();
375 if (commands->exists(cmd_num))
377 log("Found command %s in table\n", cmd_num);
378 tagged_command = *commands->find(cmd_num);
381 if (tagged_command &&
382 tagged_command->apply_response(parts, *this, storage))
383 commands->remove(cmd_num);
384 else
385 log("Applying response to command %s failed!\n", tag);
387 if (show_progress)
389 progress_notify(50 + (50 * curr_cmd++ / total_cmds));
393 if (commands->isempty() || (curr_cmd-1) == total_cmds)
395 drain();
396 break;
398 else
399 consume_newlines();
401 config.commit(); // commit the results of this iteration
404 log("%s commands left after application\n", commands->count());
405 num_retries++;
407 // This is important, because it tells our camel "provider"
408 // to complete its operation
409 if (show_progress)
411 progress_notify(100);
413 bool isempty = commands->isempty();
414 return isempty;
417 void WvExcConn::send_listusers()
419 send_command(new ListUsers(storage->get_state()[USERS_KEY]));
422 template <class Command>
423 void WvExcConn::send_acl_cmds(UniConf cfg)
425 UniConf::Iter i(cfg);
426 Command *command = NULL;
427 WvString folder;
428 WvString type;
430 for (i.rewind(); i.next(); )
432 folder = i().key().printable();
433 if (storage->get_dict()[folder])
434 type = *storage->get_dict()[folder]->get_type();
435 else
436 type = "UNKNOWN"; // uh-oh...
438 command = new Command(folder, type, storage);
439 send_command(command);
440 command = NULL;
444 void WvExcConn::send_setacls()
446 send_acl_cmds<SetACL>(storage->get_state()[SETACL_KEY]);
449 void WvExcConn::send_getacls()
451 send_acl_cmds<GetACL>(storage->get_state()[GETACL_KEY]);
454 void WvExcConn::send_createfolders()
456 CreateFolder *create_folder = NULL;
457 WvString name;
458 WvString type;
459 WvStringList folders_toadd;
461 UniConf folders_toadd_key = storage->get_state()[FOLDERS_KEY][TOADD_KEY];
462 UniConf::Iter i(folders_toadd_key);
463 for (i.rewind(); i.next();)
464 folders_toadd.append(i().key().printable());
466 // Create folders in sorted order so that nested folders get created
467 // parent-first
468 WvStringList::Sorter s(folders_toadd, &::wvstrcmp);
469 for (s.rewind(); s.next(); )
471 name = s();
472 type = folders_toadd_key[name].getme();
474 log("Folder add. Name: %s Type: %s\n", name, type);
476 create_folder = new CreateFolder(name, type);
477 send_command(create_folder);
478 create_folder = NULL;
482 void WvExcConn::send_deletefolders()
484 DeleteFolder *delete_folder = NULL;
485 WvString name;
486 WvString type;
487 WvStringList folders_todelete;
489 UniConf folders_todelete_key = storage->get_state()[FOLDERS_KEY][TODELETE_KEY];
490 UniConf::Iter j(folders_todelete_key);
491 for (j.rewind(); j.next(); )
493 log("Folder delete. Name: %s\n", j().key().printable());
495 delete_folder = new DeleteFolder(j().key().printable(), j().getme());
496 send_command(delete_folder);
497 delete_folder = NULL;
499 // Unconditionally remove the 'todelete' key: If we didn't succeed,
500 // the folder should be recreated the next time we sync
501 folders_todelete.append(j().key().printable());
504 WvStringList::Iter i(folders_todelete);
505 for (i.rewind(); i.next(); )
506 folders_todelete_key[i()].remove();
509 void WvExcConn::send_listfolders()
511 send_command(new ListFolders(storage));
514 template <class Command>
515 void WvExcConn::send_item_cmds(WvStringParm action)
517 Command *command = NULL;
519 ExchangeItAdaptorDict::Iter dict = storage->get_dict();
520 for (dict.rewind(); dict.next(); )
522 if (dict().is_ready())
524 if (dict().get_may_have_local_only_items())
525 dict().sync_local_items();
527 WvStringList *item_list = dict().get_item_uids(action);
528 log("Found %s items in %s for folder %s\n",
529 item_list->count(), action, dict().key);
531 WvStringList::Iter i(*item_list);
532 for (i.rewind(); i.next();)
534 log("Item %s for folder %s: %s\n", action, dict().key, i());
536 command = new Command(&dict(), i());
537 send_command(command);
538 command = NULL;
541 delete item_list;
546 void WvExcConn::send_deletes()
548 send_item_cmds<Delete>("todelete");
551 void WvExcConn::send_updates()
553 send_item_cmds<Update>("tosend");
556 void WvExcConn::send_syncs()
558 Sync *sync_command = NULL;
560 ExchangeItAdaptorDict::Iter dict = storage->get_dict();
561 for (dict.rewind(); dict.next(); )
563 if (dict().is_ready())
565 sync_command = new Sync(&dict());
566 send_command(sync_command);
567 sync_command = NULL;
573 @param tagged_command it deleted by the callee !
575 void WvExcConn::send_command(TaggedCommand *tagged_command, int tag)
577 if (!tagged_command)
578 log("Got a NULL command to send!\n");
580 CommandHash *commands = conn_manager->get_commands();
581 WvAutorelease<CommandHash> autoreleaser(commands);
583 if (!commands)
584 log("Command hash is gone; unable to send commands\n");
586 // Check if the user tried to cancel the sync
587 uint32_t msg = 1;
588 if (sock && sizeof(uint32_t) == sock->read(&msg, sizeof(uint32_t)))
589 if (!msg) nosend = true;
591 if (nosend || !tagged_command || !commands)
593 delete tagged_command;
594 return;
597 WvString line;
598 WvString command;
599 WvString server_command;
600 WvStringList lines;
601 WvStringList params;
603 tagged_command->get_command(command, params, lines);
605 // Tag is only to be set for commands being retried
606 if (tag)
608 log("Retrying a %s command\n", command);
609 server_command.append("%s %s", tag, command);
611 else
612 server_command.append("%s %s", cmd_tag, command);
614 if (params.count())
615 server_command.append(" %s", params.join(" "));
617 print("%s\n", server_command);
618 log(">>> (%s)\n", server_command);
620 while (lines.count())
622 line = lines.popstr();
623 print("%s\n", line);
624 log(">>> (%s)\n", line);
627 if (tagged_command->has_data())
628 tagged_command->send_data(*this);
630 print("\n");
632 // Only insert command if we're not retrying
633 if (!tag)
634 commands->add(cmd_tag++, tagged_command, true);
637 // Silly function to eat remaining blank lines from the stream. Helps us
638 // work around borken servers, and simplifies apply_resonse() functions.
639 void WvExcConn::consume_newlines()
641 WvDynBuf buf;
642 unsigned char c = 0;
643 int num = 0;
645 read(buf, 512);
646 while (buf.used())
648 c = buf.get();
649 if (c != '\n' && c != '\r')
651 buf.unget(1);
652 break;
654 num++;
657 log("Found %s newlines and ate them\n", num);
658 unread(buf, buf.used());
661 bool WvExcConn::start_sync_cb()
663 if (state == Idle)
665 progress = 0;
666 nosend = false;
667 state = Syncing;
668 alarm(0);
669 return true;
671 else
672 return false;
675 void WvExcConn::cancel_sync_cb()
677 log("cancel_sync_cb\n");
678 nosend = true;
679 have_progress_cb = false;
682 void WvExcConn::unlink_commands()
684 // FIXME: this used to invalidate the commands member, but now
685 // that this is moved to WcExcConnManager, this is made useless
686 // I don't really know we should do.
687 terminate_continue_select();
690 bool WvExcConn::pre_select(SelectInfo &si)
692 bool ret = WvStreamClone::pre_select(si);
693 bool want_close = false;
695 progress_notify(); // this will talk to camel,
696 // which should let us know if we need to quit or not
698 // If we've tried too many times OR the user cancels the sync, then reconnect
699 if (num_retries >= MAX_RETRIES || nosend)
701 want_close = true;
702 num_retries = 0;
705 // Unless we've completed a transaction, or we're still
706 // authenticating
707 if (transaction_complete || state == Authenticate)
709 want_close = false;
710 transaction_complete = false;
711 num_retries = 0;
714 if (want_close)
716 if (!nosend)
717 seterr("Failed to apply pending responses");
718 terminate_continue_select();
719 return false;
723 return ret;
726 void WvExcConn::execute()
728 WvStreamClone::execute();
730 CommandHash *commands = conn_manager->get_commands();
731 WvAutorelease<CommandHash> autoreleaser(commands);
733 if (!commands)
735 log("Command hash is gone; not doing anything.\n");
736 return;
739 // we have to authenticate before we can do anything else
740 if (state == Authenticate)
742 ErrorType auth_result;
744 log("Authenticating\n");
745 auth_result = authenticate();
746 if (auth_result != AuthOK)
748 // assume the server's gone and try again later
749 // FIXME: should tell the user, and actually differentiate
750 // between connection problems and authentication problems
751 set_errcode(auth_result);
752 log("Authentication failed !!\n");
753 return;
756 storage->load_folders(); // load (cached) folder list into memory
757 if (commands->count())
758 state = Syncing;
759 else
760 state = InitialFolderSync;
763 if (state == InitialFolderSync)
765 if (!!config[OLD_USERNAME_KEY].getme() || !!config[OLD_SERVERID_KEY].getme())
767 if (!migrate_account())
769 // call back later: not ready
770 alarm(2000);
774 log("Syncing folders for the first time!\n");
775 if (!initial_folder_sync())
777 commands->zap();
778 seterr("Couldn't perform initial folder sync");
781 state = Idle;
784 if (state == Syncing)
786 log("Syncing\n");
788 if (commands->isempty())
790 send_folder_commands();
791 apply_pending_responses(false);
794 send_sync_commands();
795 bool success = apply_pending_responses();
797 if (!transaction_complete)
798 transaction_complete = success;
800 state = Idle;
803 config.commit();