1/*
2 * Copyright 2011, Haiku, Inc. All Rights Reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Hamish Morrison <hamish@lavabit.com>
7 *		Axel D��rfler <axeld@pinc-software.de>
8 */
9
10#include "NetworkTimeView.h"
11
12#include <ctype.h>
13#include <stdio.h>
14#include <string.h>
15
16#include <Alert.h>
17#include <Button.h>
18#include <Catalog.h>
19#include <CheckBox.h>
20#include <ControlLook.h>
21#include <File.h>
22#include <FindDirectory.h>
23#include <ListView.h>
24#include <Path.h>
25#include <ScrollView.h>
26#include <TextControl.h>
27
28#include "ntp.h"
29#include "TimeMessages.h"
30
31
32#undef B_TRANSLATION_CONTEXT
33#define B_TRANSLATION_CONTEXT "Time"
34
35
36Settings::Settings()
37	:
38	fMessage(kMsgNetworkTimeSettings)
39{
40	ResetToDefaults();
41	Load();
42}
43
44
45Settings::~Settings()
46{
47	Save();
48}
49
50
51void
52Settings::AddServer(const char* server)
53{
54	if (_GetStringByValue("server", server) == B_ERROR)
55		fMessage.AddString("server", server);
56}
57
58
59const char*
60Settings::GetServer(int32 index) const
61{
62	const char* server;
63	fMessage.FindString("server", index, &server);
64	return server;
65}
66
67
68void
69Settings::RemoveServer(const char* server)
70{
71	int32 index = _GetStringByValue("server", server);
72	if (index != B_ERROR) {
73		fMessage.RemoveData("server", index);
74
75		int32 count;
76		fMessage.GetInfo("server", NULL, &count);
77		if (GetDefaultServer() >= count)
78			SetDefaultServer(count - 1);
79	}
80}
81
82
83void
84Settings::SetDefaultServer(int32 index)
85{
86	if (fMessage.ReplaceInt32("default server", index) != B_OK)
87		fMessage.AddInt32("default server", index);
88}
89
90
91int32
92Settings::GetDefaultServer() const
93{
94	int32 index;
95	fMessage.FindInt32("default server", &index);
96	return index;
97}
98
99
100void
101Settings::SetTryAllServers(bool boolean)
102{
103	fMessage.ReplaceBool("try all servers", boolean);
104}
105
106
107bool
108Settings::GetTryAllServers() const
109{
110	bool boolean;
111	fMessage.FindBool("try all servers", &boolean);
112	return boolean;
113}
114
115
116void
117Settings::SetSynchronizeAtBoot(bool boolean)
118{
119	fMessage.ReplaceBool("synchronize at boot", boolean);
120}
121
122
123bool
124Settings::GetSynchronizeAtBoot() const
125{
126	bool boolean;
127	fMessage.FindBool("synchronize at boot", &boolean);
128	return boolean;
129}
130
131
132void
133Settings::ResetServersToDefaults()
134{
135	fMessage.RemoveName("server");
136
137	fMessage.AddString("server", "pool.ntp.org");
138	fMessage.AddString("server", "de.pool.ntp.org");
139	fMessage.AddString("server", "time.nist.gov");
140
141	if (fMessage.ReplaceInt32("default server", 0) != B_OK)
142		fMessage.AddInt32("default server", 0);
143}
144
145
146void
147Settings::ResetToDefaults()
148{
149	fMessage.MakeEmpty();
150	ResetServersToDefaults();
151
152	fMessage.AddBool("synchronize at boot", true);
153	fMessage.AddBool("try all servers", true);
154}
155
156
157void
158Settings::Revert()
159{
160	fMessage = fOldMessage;
161}
162
163
164bool
165Settings::SettingsChanged()
166{
167	ssize_t oldSize = fOldMessage.FlattenedSize();
168	ssize_t newSize = fMessage.FlattenedSize();
169
170	if (oldSize != newSize || oldSize < 0 || newSize < 0)
171		return true;
172
173	char* oldBytes = new (std::nothrow) char[oldSize];
174	if (oldBytes == NULL)
175		return true;
176	fOldMessage.Flatten(oldBytes, oldSize);
177	char* newBytes = new (std::nothrow) char[newSize];
178	if (newBytes == NULL) {
179		delete[] oldBytes;
180		return true;
181	}
182	fMessage.Flatten(newBytes, newSize);
183
184	int result = memcmp(oldBytes, newBytes, oldSize);
185
186	delete[] oldBytes;
187	delete[] newBytes;
188
189	return result != 0;
190}
191
192
193status_t
194Settings::Load()
195{
196	status_t status;
197
198	BPath path;
199	if ((status = _GetPath(path)) != B_OK)
200		return status;
201
202	BFile file(path.Path(), B_READ_ONLY);
203	if ((status = file.InitCheck()) != B_OK)
204		return status;
205
206	BMessage load;
207	if ((status = load.Unflatten(&file)) != B_OK)
208		return status;
209
210	if (load.what != kMsgNetworkTimeSettings)
211		return B_BAD_TYPE;
212
213	fMessage = load;
214	fOldMessage = fMessage;
215	return B_OK;
216}
217
218
219status_t
220Settings::Save()
221{
222	status_t status;
223
224	BPath path;
225	if ((status = _GetPath(path)) != B_OK)
226		return status;
227
228	BFile file(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
229	if ((status = file.InitCheck()) != B_OK)
230		return status;
231
232	file.SetSize(0);
233
234	return fMessage.Flatten(&file);
235}
236
237
238int32
239Settings::_GetStringByValue(const char* name, const char* value)
240{
241	const char* string;
242	for (int32 index = 0; fMessage.FindString(
243		name, index, &string) == B_OK; index++)
244		if (strcmp(string, value) == 0)
245			return index;
246	return B_ERROR;
247}
248
249
250status_t
251Settings::_GetPath(BPath& path)
252{
253	status_t status = find_directory(B_USER_SETTINGS_DIRECTORY, &path);
254	if (status != B_OK)
255		return status;
256	path.Append("networktime settings");
257	return B_OK;
258}
259
260
261NetworkTimeView::NetworkTimeView(const char* name)
262	:
263	BGroupView(name, B_VERTICAL, B_USE_DEFAULT_SPACING),
264	fSettings(),
265	fUpdateThread(-1)
266{
267	fSettings.Load();
268	_InitView();
269}
270
271
272void
273NetworkTimeView::MessageReceived(BMessage* message)
274{
275	switch (message->what) {
276		case kMsgSetDefaultServer:
277		{
278			int32 sel = fServerListView->CurrentSelection();
279			if (sel < 0)
280				fServerListView->Select(fSettings.GetDefaultServer());
281			else {
282				fSettings.SetDefaultServer(sel);
283				Looper()->PostMessage(new BMessage(kMsgChange));
284			}
285			break;
286		}
287		case kMsgServerEdited:
288		{
289			rgb_color defaultColor = ui_color(B_CONTROL_TEXT_COLOR);
290			rgb_color red = {255, 0, 0};
291			int32 length = fServerTextControl->TextView()->TextLength();
292
293			if (_IsValidServerName(fServerTextControl->TextView()->Text()))
294				fServerTextControl->TextView()->SetFontAndColor(0, length, NULL, 0, &defaultColor);
295			else
296				fServerTextControl->TextView()->SetFontAndColor(0, length, NULL, 0, &red);
297
298			break;
299		}
300		case kMsgAddServer:
301			if (!_IsValidServerName(fServerTextControl->TextView()->Text()))
302				break;
303
304			fSettings.AddServer(fServerTextControl->Text());
305			_UpdateServerList();
306			fServerTextControl->SetText("");
307			Looper()->PostMessage(new BMessage(kMsgChange));
308			break;
309
310		case kMsgRemoveServer:
311			fSettings.RemoveServer(((BStringItem*)
312				fServerListView->ItemAt(
313					fServerListView->
314					CurrentSelection()))->Text());
315			_UpdateServerList();
316			Looper()->PostMessage(new BMessage(kMsgChange));
317			break;
318
319		case kMsgResetServerList:
320			fSettings.ResetServersToDefaults();
321			_UpdateServerList();
322			Looper()->PostMessage(new BMessage(kMsgChange));
323			break;
324
325		case kMsgTryAllServers:
326			fSettings.SetTryAllServers(
327				fTryAllServersCheckBox->Value());
328			Looper()->PostMessage(new BMessage(kMsgChange));
329			break;
330
331		case kMsgSynchronizeAtBoot:
332			fSettings.SetSynchronizeAtBoot(
333				fSynchronizeAtBootCheckBox->Value());
334			Looper()->PostMessage(new BMessage(kMsgChange));
335			break;
336
337		case kMsgStopSynchronization:
338			if (fUpdateThread >= B_OK)
339				kill_thread(fUpdateThread);
340			_DoneSynchronizing();
341			break;
342
343		case kMsgSynchronize:
344		{
345			if (fUpdateThread >= B_OK)
346				break;
347
348			BMessenger* messenger = new BMessenger(this);
349			update_time(fSettings, messenger, &fUpdateThread);
350			fSynchronizeButton->SetLabel(B_TRANSLATE("Stop"));
351			fSynchronizeButton->Message()->what = kMsgStopSynchronization;
352			break;
353		}
354
355		case kMsgSynchronizationResult:
356		{
357			_DoneSynchronizing();
358
359			status_t status;
360			if (message->FindInt32("status", (int32 *)&status) == B_OK) {
361				if (status == B_OK) return;
362
363				const char* errorString;
364				message->FindString("error string", &errorString);
365				char buffer[256];
366
367				int32 errorCode;
368				if (message->FindInt32("error code", &errorCode)
369					== B_OK)
370					snprintf(buffer, sizeof(buffer),
371						B_TRANSLATE("The following error occured "
372						"while synchronizing:\r\n%s: %s"),
373						errorString, strerror(errorCode));
374				else
375					snprintf(buffer, sizeof(buffer),
376						B_TRANSLATE("The following error occured "
377						"while synchronizing:\r\n%s"),
378						errorString);
379
380				BAlert* alert = new BAlert(B_TRANSLATE("Time"), buffer,
381					B_TRANSLATE("OK"));
382				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
383				alert->Go();
384			}
385			break;
386		}
387
388		case kMsgRevert:
389			fSettings.Revert();
390		    fTryAllServersCheckBox->SetValue(
391		    	fSettings.GetTryAllServers());
392			fSynchronizeAtBootCheckBox->SetValue(
393				fSettings.GetSynchronizeAtBoot());
394			_UpdateServerList();
395			break;
396	}
397}
398
399
400void
401NetworkTimeView::AttachedToWindow()
402{
403	fServerTextControl->SetTarget(this);
404	fServerListView->SetTarget(this);
405	fAddButton->SetTarget(this);
406	fRemoveButton->SetTarget(this);
407	fResetButton->SetTarget(this);
408	fTryAllServersCheckBox->SetTarget(this);
409	fSynchronizeAtBootCheckBox->SetTarget(this);
410	fSynchronizeButton->SetTarget(this);
411}
412
413
414bool
415NetworkTimeView::CheckCanRevert()
416{
417	return fSettings.SettingsChanged();
418}
419
420
421void
422NetworkTimeView::_InitView()
423{
424	fServerTextControl = new BTextControl(NULL, NULL, new BMessage(kMsgAddServer));
425	fServerTextControl->SetModificationMessage(new BMessage(kMsgServerEdited));
426
427	fAddButton = new BButton("add", B_TRANSLATE("Add"),
428		new BMessage(kMsgAddServer));
429	fRemoveButton = new BButton("remove", B_TRANSLATE("Remove"),
430		new BMessage(kMsgRemoveServer));
431	fResetButton = new BButton("reset", B_TRANSLATE("Reset"),
432		new BMessage(kMsgResetServerList));
433
434	fServerListView = new BListView("serverList");
435	fServerListView->SetSelectionMessage(new
436		BMessage(kMsgSetDefaultServer));
437	BScrollView* scrollView = new BScrollView("serverScrollView",
438		fServerListView, B_FRAME_EVENTS | B_WILL_DRAW, false, true);
439	_UpdateServerList();
440
441	fTryAllServersCheckBox = new BCheckBox("tryAllServers",
442		B_TRANSLATE("Try all servers"), new BMessage(kMsgTryAllServers));
443	fTryAllServersCheckBox->SetValue(fSettings.GetTryAllServers());
444
445	fSynchronizeAtBootCheckBox = new BCheckBox("autoUpdate",
446		B_TRANSLATE("Synchronize at boot"),
447		new BMessage(kMsgSynchronizeAtBoot));
448	fSynchronizeAtBootCheckBox->SetValue(fSettings.GetSynchronizeAtBoot());
449	fSynchronizeButton = new BButton("update", B_TRANSLATE("Synchronize"),
450		new BMessage(kMsgSynchronize));
451	fSynchronizeButton->SetExplicitAlignment(
452		BAlignment(B_ALIGN_RIGHT, B_ALIGN_BOTTOM));
453
454	const float kInset = be_control_look->DefaultItemSpacing();
455	BLayoutBuilder::Group<>(this)
456		.AddGroup(B_HORIZONTAL)
457			.AddGroup(B_VERTICAL, 0)
458				.Add(fServerTextControl)
459				.Add(scrollView)
460			.End()
461			.AddGroup(B_VERTICAL, kInset / 2)
462				.Add(fAddButton)
463				.Add(fRemoveButton)
464				.Add(fResetButton)
465				.AddGlue()
466			.End()
467		.End()
468		.AddGroup(B_HORIZONTAL)
469			.AddGroup(B_VERTICAL, 0)
470				.Add(fTryAllServersCheckBox)
471				.Add(fSynchronizeAtBootCheckBox)
472			.End()
473			.Add(fSynchronizeButton)
474		.End()
475		.SetInsets(kInset, kInset, kInset, kInset);
476}
477
478
479void
480NetworkTimeView::_UpdateServerList()
481{
482	while (fServerListView->RemoveItem((int32)0) != NULL);
483
484	const char* server;
485	int32 index = 0;
486
487	while ((server = fSettings.GetServer(index++)) != NULL)
488		fServerListView->AddItem(new BStringItem(server));
489
490	fServerListView->Select(fSettings.GetDefaultServer());
491	fServerListView->ScrollToSelection();
492}
493
494
495void
496NetworkTimeView::_DoneSynchronizing()
497{
498	fUpdateThread = -1;
499	fSynchronizeButton->SetLabel(B_TRANSLATE("Synchronize again"));
500	fSynchronizeButton->Message()->what = kMsgSynchronize;
501}
502
503
504bool
505NetworkTimeView::_IsValidServerName(const char * serverName)
506{
507	if (serverName[0] == '\0')
508		return false;
509
510	for (int32 i = 0; serverName[i] != '\0'; i++) {
511		char c = serverName[i];
512		// Simple URL validation, no scheme should be present
513		if (!(isalnum(c) || c == '.' || c == '-' || c == '_'))
514			return false;
515	}
516
517	return true;
518}
519
520
521status_t
522update_time(const Settings& settings, const char** errorString,
523	int32* errorCode)
524{
525	int32 defaultServer = settings.GetDefaultServer();
526
527	status_t status = B_ENTRY_NOT_FOUND;
528	const char* server = settings.GetServer(defaultServer);
529
530	if (server != NULL)
531		status = ntp_update_time(server, errorString, errorCode);
532
533	if (status != B_OK && settings.GetTryAllServers()) {
534		for (int32 index = 0; ; index++) {
535			if (index == defaultServer)
536				index++;
537			server = settings.GetServer(index);
538			if (server == NULL)
539				break;
540
541			status = ntp_update_time(server, errorString, errorCode);
542			if (status == B_OK)
543				break;
544		}
545	}
546
547	return status;
548}
549
550
551int32
552update_thread(void* params)
553{
554	BList* list = (BList*)params;
555	BMessenger* messenger = (BMessenger*)list->ItemAt(1);
556
557	const char* errorString = NULL;
558	int32 errorCode = 0;
559	status_t status = update_time(*(Settings*)list->ItemAt(0),
560		&errorString, &errorCode);
561
562	BMessage result(kMsgSynchronizationResult);
563	result.AddInt32("status", status);
564	result.AddString("error string", errorString);
565	if (errorCode != 0)
566		result.AddInt32("error code", errorCode);
567	messenger->SendMessage(&result);
568
569	delete messenger;
570	return B_OK;
571}
572
573
574status_t
575update_time(const Settings& settings, BMessenger* messenger,
576	thread_id* thread)
577{
578	BList* params = new BList(2);
579	params->AddItem((void*)&settings);
580	params->AddItem((void*)messenger);
581	*thread = spawn_thread(update_thread, "ntpUpdate", 64, params);
582	return resume_thread(*thread);
583}
584
585