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