/*
 * Copyright 2011-2014 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Axel Dörfler, axeld@pinc-software.de
 *		Hamish Morrison, hamish@lavabit.com
 *		John Scipione, jscipione@gmail.com
 */
 
 
#include "NetworkTimeView.h"
 
#include <ctype.h>
#include <stdio.h>
#include <string.h>
 
#include <Alert.h>
#include <Button.h>
#include <Catalog.h>
#include <CheckBox.h>
#include <File.h>
#include <FindDirectory.h>
#include <Invoker.h>
#include <ListItem.h>
#include <ListView.h>
#include <Path.h>
#include <ScrollView.h>
#include <Size.h>
#include <TextControl.h>
 
#include "ntp.h"
#include "TimeMessages.h"
 
 
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Time"
 
 
//	#pragma mark - Settings
 
 
Settings::Settings()
	:
	fMessage(kMsgNetworkTimeSettings)
{
	ResetToDefaults();
	Load();
}
 
 
Settings::~Settings()
{
	Save();
}
 
 
void
Settings::AddServer(const char* server)
{
	if (_GetStringByValue("server", server) == B_ERROR)
		fMessage.AddString("server", server);
}
 
 
const char*
Settings::GetServer(int32 index) const
{
	const char* server;
	fMessage.FindString("server", index, &server);
	return server;
}
 
 
void
Settings::RemoveServer(const char* server)
{
	int32 index = _GetStringByValue("server", server);
	if (index != B_ERROR) {
		fMessage.RemoveData("server", index);
 
		int32 count;
		fMessage.GetInfo("server", NULL, &count);
		if (GetDefaultServer() >= count)
			SetDefaultServer(count - 1);
	}
}
 
 
void
Settings::SetDefaultServer(int32 index)
{
	if (fMessage.ReplaceInt32("default server", index) != B_OK)
		fMessage.AddInt32("default server", index);
}
 
 
int32
Settings::GetDefaultServer() const
{
	int32 index;
	fMessage.FindInt32("default server", &index);
	return index;
}
 
 
void
Settings::SetTryAllServers(bool boolean)
{
	fMessage.ReplaceBool("try all servers", boolean);
}
 
 
bool
Settings::GetTryAllServers() const
{
	bool boolean;
	fMessage.FindBool("try all servers", &boolean);
	return boolean;
}
 
 
void
Settings::SetSynchronizeAtBoot(bool boolean)
{
	fMessage.ReplaceBool("synchronize at boot", boolean);
}
 
 
bool
Settings::GetSynchronizeAtBoot() const
{
	bool boolean;
	fMessage.FindBool("synchronize at boot", &boolean);
	return boolean;
}
 
 
void
Settings::ResetServersToDefaults()
{
	fMessage.RemoveName("server");
 
	fMessage.AddString("server", "pool.ntp.org");
	fMessage.AddString("server", "de.pool.ntp.org");
	fMessage.AddString("server", "time.nist.gov");
 
	if (fMessage.ReplaceInt32("default server", 0) != B_OK)
		fMessage.AddInt32("default server", 0);
}
 
 
void
Settings::ResetToDefaults()
{
	fMessage.MakeEmpty();
	ResetServersToDefaults();
 
	fMessage.AddBool("synchronize at boot", true);
	fMessage.AddBool("try all servers", true);
}
 
 
void
Settings::Revert()
{
	fMessage = fOldMessage;
}
 
 
bool
Settings::SettingsChanged()
{
	ssize_t oldSize = fOldMessage.FlattenedSize();
	ssize_t newSize = fMessage.FlattenedSize();
 
	if (oldSize != newSize || oldSize < 0 || newSize < 0)
		return true;
 
	char* oldBytes = new (std::nothrow) char[oldSize];
	if (oldBytes == NULL)
		return true;
 
	fOldMessage.Flatten(oldBytes, oldSize);
	char* newBytes = new (std::nothrow) char[newSize];
	if (newBytes == NULL) {
		delete[] oldBytes;
		return true;
	}
	fMessage.Flatten(newBytes, newSize);
 
	int result = memcmp(oldBytes, newBytes, oldSize);
 
	delete[] oldBytes;
	delete[] newBytes;
 
	return result != 0;
}
 
 
status_t
Settings::Load()
{
	status_t status;
 
	BPath path;
	if ((status = _GetPath(path)) != B_OK)
		return status;
 
	BFile file(path.Path(), B_READ_ONLY);
	if ((status = file.InitCheck()) != B_OK)
		return status;
 
	BMessage load;
	if ((status = load.Unflatten(&file)) != B_OK)
		return status;
 
	if (load.what != kMsgNetworkTimeSettings)
		return B_BAD_TYPE;
 
	fMessage = load;
	fOldMessage = fMessage;
	return B_OK;
}
 
 
status_t
Settings::Save()
{
	status_t status;
 
	BPath path;
	if ((status = _GetPath(path)) != B_OK)
		return status;
 
	BFile file(path.Path(), B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
	if ((status = file.InitCheck()) != B_OK)
		return status;
 
	file.SetSize(0);
 
	return fMessage.Flatten(&file);
}
 
 
int32
Settings::_GetStringByValue(const char* name, const char* value)
{
	const char* string;
	for (int32 index = 0; fMessage.FindString(name, index, &string) == B_OK;
			index++) {
		if (strcmp(string, value) == 0)
			return index;
	}
 
	return B_ERROR;
}
 
 
status_t
Settings::_GetPath(BPath& path)
{
	status_t status = find_directory(B_USER_SETTINGS_DIRECTORY, &path);
	if (status != B_OK)
		return status;
 
	path.Append("networktime settings");
 
	return B_OK;
}
 
 
//	#pragma mark - NetworkTimeView
 
 
NetworkTimeView::NetworkTimeView(const char* name)
	:
	BGroupView(name, B_VERTICAL, B_USE_DEFAULT_SPACING),
	fSettings(),
	fServerTextControl(NULL),
	fAddButton(NULL),
	fRemoveButton(NULL),
	fResetButton(NULL),
	fServerListView(NULL),
	fTryAllServersCheckBox(NULL),
	fSynchronizeAtBootCheckBox(NULL),
	fSynchronizeButton(NULL),
	fTextColor(ui_color(B_CONTROL_TEXT_COLOR)),
	fInvalidColor(ui_color(B_FAILURE_COLOR)),
	fUpdateThread(-1)
{
	fSettings.Load();
	_InitView();
}
 
 
NetworkTimeView::~NetworkTimeView()
{
	delete fServerTextControl;
	delete fAddButton;
	delete fRemoveButton;
	delete fResetButton;
	delete fServerListView;
	delete fTryAllServersCheckBox;
	delete fSynchronizeAtBootCheckBox;
	delete fSynchronizeButton;
}
 
 
void
NetworkTimeView::MessageReceived(BMessage* message)
{
	switch (message->what) {
		case kMsgSetDefaultServer:
		{
			int32 currentSelection = fServerListView->CurrentSelection();
			if (currentSelection < 0)
				fServerListView->Select(fSettings.GetDefaultServer());
			else {
				fSettings.SetDefaultServer(currentSelection);
				Looper()->PostMessage(new BMessage(kMsgChange));
			}
			break;
		}
 
		case kMsgServerEdited:
		{
			bool isValid = _IsValidServerName(fServerTextControl->Text());
			fServerTextControl->TextView()->SetFontAndColor(0,
				fServerTextControl->TextView()->TextLength(), NULL, 0,
				isValid ? &fTextColor : &fInvalidColor);
			fAddButton->SetEnabled(isValid);
			break;
		}
 
		case kMsgAddServer:
			if (!_IsValidServerName(fServerTextControl->Text()))
				break;
 
			fSettings.AddServer(fServerTextControl->Text());
			_UpdateServerList();
			fServerTextControl->SetText("");
			Looper()->PostMessage(new BMessage(kMsgChange));
			break;
 
		case kMsgRemoveServer:
		{
			int32 currentSelection = fServerListView->CurrentSelection();
			if (currentSelection < 0)
				break;
 
			fSettings.RemoveServer(((BStringItem*)
				fServerListView->ItemAt(currentSelection))->Text());
			_UpdateServerList();
			Looper()->PostMessage(new BMessage(kMsgChange));
			break;
		}
 
		case kMsgResetServerList:
			fSettings.ResetServersToDefaults();
			_UpdateServerList();
			Looper()->PostMessage(new BMessage(kMsgChange));
			break;
 
		case kMsgTryAllServers:
			fSettings.SetTryAllServers(
				fTryAllServersCheckBox->Value());
			Looper()->PostMessage(new BMessage(kMsgChange));
			break;
 
		case kMsgSynchronizeAtBoot:
			fSettings.SetSynchronizeAtBoot(fSynchronizeAtBootCheckBox->Value());
			Looper()->PostMessage(new BMessage(kMsgChange));
			break;
 
		case kMsgStopSynchronization:
			if (fUpdateThread >= B_OK)
				kill_thread(fUpdateThread);
 
			_DoneSynchronizing();
			break;
 
		case kMsgSynchronize:
		{
			if (fUpdateThread >= B_OK)
				break;
 
			BMessenger* messenger = new BMessenger(this);
			update_time(fSettings, messenger, &fUpdateThread);
			fSynchronizeButton->SetLabel(B_TRANSLATE("Stop"));
			fSynchronizeButton->Message()->what = kMsgStopSynchronization;
			break;
		}
 
		case kMsgSynchronizationResult:
		{
			_DoneSynchronizing();
 
			status_t status;
			if (message->FindInt32("status", (int32 *)&status) == B_OK) {
				if (status == B_OK)
					return;
 
				const char* errorString;
				message->FindString("error string", &errorString);
				char buffer[256];
 
				int32 errorCode;
				if (message->FindInt32("error code", &errorCode) == B_OK) {
					snprintf(buffer, sizeof(buffer),
						B_TRANSLATE("The following error occured "
							"while synchronizing:\r\n%s: %s"),
						errorString, strerror(errorCode));
				} else {
					snprintf(buffer, sizeof(buffer),
						B_TRANSLATE("The following error occured "
							"while synchronizing:\r\n%s"),
						errorString);
				}
 
				BAlert* alert = new BAlert(B_TRANSLATE("Time"), buffer,
					B_TRANSLATE("OK"));
				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
				alert->Go();
			}
			break;
		}
 
		case kMsgRevert:
			fSettings.Revert();
			fTryAllServersCheckBox->SetValue(fSettings.GetTryAllServers());
			fSynchronizeAtBootCheckBox->SetValue(
				fSettings.GetSynchronizeAtBoot());
			_UpdateServerList();
			break;
 
		default:
			BGroupView::MessageReceived(message);
			break;
	}
}
 
 
void
NetworkTimeView::AttachedToWindow()
{
	fServerTextControl->SetTarget(this);
	fServerListView->SetTarget(this);
	fAddButton->SetTarget(this);
	fAddButton->SetEnabled(false);
	fRemoveButton->SetTarget(this);
	fResetButton->SetTarget(this);
	fTryAllServersCheckBox->SetTarget(this);
	fSynchronizeAtBootCheckBox->SetTarget(this);
	fSynchronizeButton->SetTarget(this);
}
 
 
bool
NetworkTimeView::CheckCanRevert()
{
	return fSettings.SettingsChanged();
}
 
 
void
NetworkTimeView::_InitView()
{
	fServerTextControl = new BTextControl(NULL, NULL,
		new BMessage(kMsgAddServer));
	fServerTextControl->SetModificationMessage(new BMessage(kMsgServerEdited));
 
	const float kButtonWidth = fServerTextControl->Frame().Height();
 
	fAddButton = new BButton("add", "+", new BMessage(kMsgAddServer));
	fAddButton->SetToolTip(B_TRANSLATE("Add"));
	fAddButton->SetExplicitSize(BSize(kButtonWidth, kButtonWidth));
 
	fRemoveButton = new BButton("remove", "−", new BMessage(kMsgRemoveServer));
	fRemoveButton->SetToolTip(B_TRANSLATE("Remove"));
	fRemoveButton->SetExplicitSize(BSize(kButtonWidth, kButtonWidth));
 
	fServerListView = new BListView("serverList");
	fServerListView->SetExplicitMinSize(BSize(B_SIZE_UNSET, kButtonWidth * 4));
	fServerListView->SetSelectionMessage(new BMessage(kMsgSetDefaultServer));
	BScrollView* scrollView = new BScrollView("serverScrollView",
		fServerListView, B_FRAME_EVENTS | B_WILL_DRAW, false, true);
	_UpdateServerList();
 
	fTryAllServersCheckBox = new BCheckBox("tryAllServers",
		B_TRANSLATE("Try all servers"), new BMessage(kMsgTryAllServers));
	fTryAllServersCheckBox->SetValue(fSettings.GetTryAllServers());
 
	fSynchronizeAtBootCheckBox = new BCheckBox("autoUpdate",
		B_TRANSLATE("Synchronize at boot"),
		new BMessage(kMsgSynchronizeAtBoot));
	fSynchronizeAtBootCheckBox->SetValue(fSettings.GetSynchronizeAtBoot());
 
	fResetButton = new BButton("reset",
		B_TRANSLATE("Reset to default server list"),
		new BMessage(kMsgResetServerList));
 
	fSynchronizeButton = new BButton("update", B_TRANSLATE("Synchronize"),
		new BMessage(kMsgSynchronize));
 
	BLayoutBuilder::Group<>(this, B_VERTICAL)
		.AddGroup(B_VERTICAL, B_USE_SMALL_SPACING)
			.AddGroup(B_HORIZONTAL, B_USE_SMALL_SPACING)
				.Add(fServerTextControl)
				.Add(fAddButton)
			.End()
			.AddGroup(B_HORIZONTAL, B_USE_SMALL_SPACING)
				.Add(scrollView)
				.AddGroup(B_VERTICAL, B_USE_SMALL_SPACING)
					.Add(fRemoveButton)
					.AddGlue()
				.End()
			.End()
		.End()
		.AddGroup(B_HORIZONTAL)
			.AddGroup(B_VERTICAL, 0)
				.Add(fTryAllServersCheckBox)
				.Add(fSynchronizeAtBootCheckBox)
			.End()
		.End()
		.AddGroup(B_HORIZONTAL)
			.AddGlue()
			.Add(fResetButton)
			.Add(fSynchronizeButton)
		.End()
		.SetInsets(B_USE_WINDOW_SPACING, B_USE_WINDOW_SPACING,
			B_USE_WINDOW_SPACING, B_USE_DEFAULT_SPACING);
}
 
 
void
NetworkTimeView::_UpdateServerList()
{
	BListItem* item;
	while ((item = fServerListView->RemoveItem((int32)0)) != NULL)
		delete item;
 
	const char* server;
	int32 index = 0;
	while ((server = fSettings.GetServer(index++)) != NULL)
		fServerListView->AddItem(new BStringItem(server));
 
	fServerListView->Select(fSettings.GetDefaultServer());
	fServerListView->ScrollToSelection();
 
	fRemoveButton->SetEnabled(fServerListView->CountItems() > 0);
}
 
 
void
NetworkTimeView::_DoneSynchronizing()
{
	fUpdateThread = -1;
	fSynchronizeButton->SetLabel(B_TRANSLATE("Synchronize again"));
	fSynchronizeButton->Message()->what = kMsgSynchronize;
}
 
 
bool
NetworkTimeView::_IsValidServerName(const char* serverName)
{
	if (serverName == NULL || *serverName == '\0')
		return false;
 
	for (int32 i = 0; serverName[i] != '\0'; i++) {
		char c = serverName[i];
		// Simple URL validation, no scheme should be present
		if (!(isalnum(c) || c == '.' || c == '-' || c == '_'))
			return false;
	}
 
	return true;
}
 
 
//	#pragma mark - update functions
 
 
int32
update_thread(void* params)
{
	BList* list = (BList*)params;
	BMessenger* messenger = (BMessenger*)list->ItemAt(1);
 
	const char* errorString = NULL;
	int32 errorCode = 0;
	status_t status = update_time(*(Settings*)list->ItemAt(0),
		&errorString, &errorCode);
 
	BMessage result(kMsgSynchronizationResult);
	result.AddInt32("status", status);
	result.AddString("error string", errorString);
	if (errorCode != 0)
		result.AddInt32("error code", errorCode);
 
	messenger->SendMessage(&result);
	delete messenger;
 
	return B_OK;
}
 
 
status_t
update_time(const Settings& settings, BMessenger* messenger,
	thread_id* thread)
{
	BList* params = new BList(2);
	params->AddItem((void*)&settings);
	params->AddItem((void*)messenger);
	*thread = spawn_thread(update_thread, "ntpUpdate", 64, params);
 
	return resume_thread(*thread);
}
 
 
status_t
update_time(const Settings& settings, const char** errorString,
	int32* errorCode)
{
	int32 defaultServer = settings.GetDefaultServer();
 
	status_t status = B_ENTRY_NOT_FOUND;
	const char* server = settings.GetServer(defaultServer);
 
	if (server != NULL)
		status = ntp_update_time(server, errorString, errorCode);
 
	if (status != B_OK && settings.GetTryAllServers()) {
		for (int32 index = 0; ; index++) {
			if (index == defaultServer)
				index++;
 
			server = settings.GetServer(index);
			if (server == NULL)
				break;
 
			status = ntp_update_time(server, errorString, errorCode);
			if (status == B_OK)
				break;
		}
	}
 
	return status;
}

V773 Visibility scope of the 'alert' pointer was exited without releasing the memory. A memory leak is possible.