/*+++*
 *  RCS Controller.m,v 1.7 1995/07/13 11:37:03 tom Exp
 *  title:	Controller.m
 *  abstract:	implementatiopn of Controller class, for NewsConfig.app
 *  author:	T.R.Hageman, Groningen, The Netherlands
 *  created:	December 1994
 *  modified:	(see RCS Log at end)
 *  copyright:
 *
 *		Copyright (C) 1994,1995  Tom R. Hageman.
 *
 *	This is free software; you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation; either version 2 of the License, or
 *	(at your option) any later version.
 *
 *	This software is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with this software; if not, write to the Free Software
 *	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *  description:
 *	(see corresponding *.h file)
 *---*/

#import "Controller.h"
#import "CNewsConfigPanel.h"
#import "version.h"

#import <ansi.h>
#include <sys/types.h>
#include <sys/stat.h>
/* Posix-style flags, are not defined by default.  Sigh... */
#ifndef S_ISUID
#   define S_ISGID	02000
#endif
#ifndef S_ISGID
#   define S_ISUID	04000
#endif

#include <pwd.h>

#import <nikit/NILoginPanel.h>


@interface HashTable (FILEIO)
- readFromFILE:(FILE *)stream;
- writeToFILE:(FILE *)stream;
@end

@interface Controller (Private)
- (BOOL)_checkUID:(int)uid;
- (BOOL)_runLoginPanel;
// NILoginPanel delegate method:
- (BOOL)panel:sender authenticateUser:(const char *)userName
	withPassword:(const char *)password inDomain:(const void *)domain;
- (const char *)_command:(char *)buf name:(const char *)name;
- (BOOL)_setPrivileged:(BOOL)wantPrivilege;
@end

#define COMMANDLEN	(3*(MAXPATHLEN+1) + 15)

@implementation Controller

static const char *newsconfig;

// Application delegates

- appWillInit:sender
{
	int euid = geteuid();

	if (![self _checkUID:euid]) {
		if (NXRunAlertPanel("Not Running As News",
				    "%s has not been installed SUID news."
				    "  This means you can only examine the news"
				    " configuration, not change it.",
				    "Continue", "Quit", NULL,
				    [NXApp appName]) != NX_ALERTDEFAULT) {
			[NXApp terminate:self];
		}
		[configPanel setEnabled:NO];
	}
	else if (euid == 0) {
		struct passwd *pw = getpwnam("news");

		if (NXRunAlertPanel("Install as SUID News",
				    "Installing %s as SUID news allows any user"
				    " to change the news configuration"
				    " (if (s)he knows the root password).",
				    "Install", "Cancel", NULL,
				    [NXApp appName]) ==
		    NX_ALERTDEFAULT) {
			struct stat st;

			if (stat(NXArgv[0], &st) >= 0 &&
			    chown(NXArgv[0], pw->pw_uid, pw->pw_gid) >= 0 &&
			    chmod(NXArgv[0], S_ISUID|S_ISGID|st.st_mode) >= 0) {
				NXRunAlertPanel("Installation Successful",
						"Program installed successfully.",
						"OK", NULL, NULL);
			}
		}
		// Make myself run as news.
		setegid(pw->pw_gid);
		seteuid(pw->pw_uid);
	}
	/* We give up privileged status right at the start so that for the
	   most part we're running as a mortal user.  Reclaim privileged
	   status only when it's absolutely necessary (i.e., when we're
	   about read or write the news configuration).

	   We have to suffer through all this chullabaloo because the
	   context-sensitive Help key does not work if euid != the uid of
	   the user logged in at the console.
	   Yet Another Nice Surprise Brought To You by NeXT, Inc...
	*/
	[self _setPrivileged:NO];
	return self;
}

- appDidInit:sender
{
	HashTable *table = [[HashTable alloc] initKeyDesc:"*" valueDesc:"*"];
	FILE *process;
	char cmd[COMMANDLEN+1];
	int exitcode;

	// Find newsconfig file.
	static const char *configpaths[] = {
		"/usr/local/lib/news/bin/config",
		"/usr/lib/news/bin/config",
		"/usr/local/news/bin/config",
		"/usr/news/bin/config",
		NULL
	};
	const char **p = configpaths;

	while ((newsconfig = *p++) != NULL && access(newsconfig, F_OK) < 0) ;

	if (newsconfig == NULL) {

		NXRunAlertPanel("Error", "Cannot find news config in the usual places (/usr/local/lib /usr/lib /usr/local/news /usr/news).  Is news installed?",
				"Quit", NULL, NULL);
		// XXX Bring up Open panel?
		[NXApp terminate:self];
		return nil;
	}

	[self _setPrivileged:YES];
	// Get the configuration in table.
	if ((process = popen([self _command:cmd name:"getnewsconf.sh"], "r")) == NULL) {
		NXRunAlertPanel("Fatal Error", "Cannot run `%s' (%s)",
				"Quit", NULL, NULL,
				cmd, strerror(errno));
		[NXApp terminate:self];
		return nil;
	}
	[self _setPrivileged:NO];

	[table readFromFILE:process];
	if ((exitcode = pclose(process)) != 0) {
		NXRunAlertPanel("Fatal Error", "Execution of `%s' failed (exit code %d)",
				"Quit", NULL, NULL,
				cmd, exitcode);
		[NXApp terminate:self];
		return nil;
	}

	[configPanel setConfiguration:table];
	[configPanel orderFront:self];
	return self;
}

// Action targets
- configure:sender
{
	// We are running unprivileged now, so euid == initial real uid here
	if ([self _checkUID:geteuid()] || [self _runLoginPanel]) {
		// Adapt the C-News configuration according to the info in table.
		HashTable *table = [configPanel configuration];
		FILE *process;
		char cmd[COMMANDLEN+1];
		int exitcode;

		[self _setPrivileged:YES];
		// Get the configuration in table.
		if ((process = popen([self _command:cmd name:"setnewsconf.sh"],  "w")) == NULL) {
			NXRunAlertPanel("Configuration Failed", "Cannot run `%s' (%s)",
					"Quit", NULL, NULL,
					cmd, strerror(errno));
			[NXApp terminate:self];
			return nil;
		}
		[self _setPrivileged:NO];

		[table writeToFILE:process];
#define EXIT_LOCKED	99
		fprintf(process, "EXIT_LOCKED	%d\n", EXIT_LOCKED); // Quick hack.
		
		if ((exitcode = pclose(process)) != 0) {
			if (NXRunAlertPanel("Configuration Failed",
					    (exitcode == EXIT_LOCKED << 8) ?
					    "News system is currently locked (try again later...)\n" :
					    "Execution of `%s' failed with exit code %d.\n",
					    "Quit", "Cancel", NULL,
					    cmd, exitcode) == NX_ALERTDEFAULT) {
				[NXApp terminate:self];
			}
			return nil;
		}
		[configPanel setConfiguration:table];
		[configPanel orderFront:self];
	}
//	[NXApp terminate:self];
    	return self;
}

- showInfo:sender
{
	if (infoPanel == nil)
	{
		[NXApp loadNibSection:"Info.nib" owner:self];
		[infoVersionField setStringValue:VERSION];
	}
	[infoPanel makeKeyAndOrderFront:self];
	return self;
}

@end // Controller


@implementation Controller (Private)

- (BOOL)_checkUID:(int)uid
{
	int newsuid;
	struct passwd *pw = getpwnam("news");

	if (pw == NULL) {
		// XXX error handling.
		return NO;
	}
	newsuid = pw->pw_uid;
	return (BOOL)(uid == 0 /*root*/ || uid == newsuid ||
		      ((pw = getpwnam("usenet")) && uid == pw->pw_uid));
		      // Assume usenet is owner of news with group news
		      // and members of group news are allowed to write config.
}

#define MAX_LOGIN_RETRIES	3

static char retryLogin;	// Support variables, set by loginPanel validation.
static BOOL cancelLogin;

- (BOOL)_runLoginPanel
{
	char instructions[100];
	NILoginPanel *loginpanel = [NILoginPanel new];

	[loginpanel setDelegate:self];

	// Build instructions.
	sprintf(instructions,
		"Please supply the password of \"root\"%s or \"news\".",
		getpwnam("usenet") ? ", \"usenet\"" : "");

	retryLogin = MAX_LOGIN_RETRIES;
	do {
		cancelLogin = YES;
		if ([loginpanel runModalWithValidation:self inDomain:NULL
		     withUser:"" withInstruction:instructions
		     allowChange:NO]) {
			return YES;
		}
	} while (!cancelLogin);

	return NO;
}

// Delegate method for user authentication panel.

- (BOOL)panel:sender authenticateUser:(const char *)userName
	withPassword:(const char *)password inDomain:(const void *)domain
{
	const char *allowed_users[] = { "root", "news", "usenet", NULL };
	const char **users = allowed_users;
	const char *user;

	while ((user = *users++) != NULL) {
		struct passwd *pw;
		
		if ((pw = getpwnam(user)) == NULL) continue;

		if (strcmp(crypt((char *)password, pw->pw_passwd),
			   pw->pw_passwd) == 0)
			return YES;
	}

	// Alert the user here, instead of in _runAlertPanel, since
	// [loginPanel runModal...] also returns NO if the user pressed
	// Cancel, and we have no way to distinguish between that
	// and an invalid password there.
	if (--retryLogin == 0) {
		NXRunAlertPanel("Incorrect Password",
				"That was one time too many."
				"  You've worn out your welcome, I'm afraid...",
				 "ByeBye", NULL, NULL);
		[NXApp terminate:self];
	}
	else if (NXRunAlertPanel("Incorrect Password",
				 "Sorry, you supplied an incorrect password.",
				 "Retry", "Give Up", NULL) == NX_ALERTDEFAULT) {
		cancelLogin = NO;
	}

	return NO;
}


- (const char *)_command:(char *)buf name:(const char *)name
{
	char path[MAXPATHLEN+1];

	[[NXBundle mainBundle] getPath:path forResource:name ofType:NULL];
	sprintf(buf, "NEWSCONFIG=%s %s %s",
		newsconfig, path, [[NXBundle mainBundle] directory]);
	return buf;
}

// Set privileged status, assuming the program is installed setuid.
// This is a no-op if the program is not installed setuid.
// Returns previous privilege status.

- (BOOL)_setPrivileged:(BOOL)wantPrivilege
{
	static int privilegedUid = -1;
	int currentEffectiveUid = geteuid();

	if (privilegedUid == -1)		// first time around.
	{
		privilegedUid = currentEffectiveUid;
	}

	if ((wantPrivilege && currentEffectiveUid != privilegedUid) ||
	    (!wantPrivilege && currentEffectiveUid == privilegedUid))
	{
		// Interchange real and effective uid.
		setreuid(currentEffectiveUid, getuid());
		return !wantPrivilege;
	}

	// No change.
	return wantPrivilege;
}

@end // Controller (Private)


@implementation HashTable (FILEIO)

- readFromFILE:(FILE *)file
{
	NXStream *stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	int c;

	while ((c = getc(file)) >= 0) {
		const char *key;
		char *value;
		int dummy;

		if (NXIsSpace(c)) continue;
		if (c == '#') {
			while ((c = getc(file)) >= 0 && c != '\n') ;
			continue;
		}
		// Found key; collect it.
		NXSeek(stream, 0, NX_FROMSTART);
		do {
			NXPutc(stream, c);
		} while ((c = getc(file)) >= 0 && !NXIsSpace(c));
		NXPutc(stream, '\0');
		NXGetMemoryBuffer(stream, &key, &dummy, &dummy);
		key = NXCopyStringBufferFromZone(key, [self zone]);

		// Skip over spaces and tabs.
		while (c == ' ' || c == '\t') c = getc(file);

		// Value is rest of line, up to newline.
		NXSeek(stream, 0, NX_FROMSTART);
		if (c != '\n') do {
			NXPutc(stream, c);
		} while ((c = getc(file)) >= 0 && c != '\n');
		NXPutc(stream, '\0');
		NXGetMemoryBuffer(stream, &value, &dummy, &dummy);
		value = NXCopyStringBufferFromZone(value, [self zone]);

		// Insert key/value pair in hashtable.
#if DEBUG
		fprintf(stderr, "key:%s value:%s\n", key, value);
#endif
		[self insertKey:key value:value];
	}
	NXCloseMemory(stream, NX_FREEBUFFER);
	return self;
}

- writeToFILE:(FILE *)file
{
	const void *key;
        void *value;
	NXHashState state = [self initState];

	while ([self nextState: &state key:&key value:&value]) {
		fprintf(file, "%s\t%s\n", (const char *)key, (char *)value);
	}
	return self;
}

@end // HashTable (FILEIO)

//======================================================================
// Controller.m,v
// Revision 1.7  1995/07/13 11:37:03  tom
// (-appWillInit:): use [configPanel setEnabled] instead of [okButton ...];
// (-configure:): add EXIT_LOCKED kludge;
// (-_runLoginPanel,): allow Cancel from login panel;
// [added RCS Id, fixed Copyright notice]
//
// Revision 1.6  1995/01/27  03:51:30  tom
// (_setPrivileged): new support method; (appWillInit): use it, set e[ug]id
//  instead of set[ug]id to keep privileged mode;
// (appDidInit:,configure): use it; (_runLoginPanel): limit nuber of retries;
// Program must run as real user for Help (Ctrl+Alt) key to work: it doesn't
// work otherwise if the program is setuid news. (unless you're logged in
// as news or root, presumably...)
//
// Revision 1.5  1995/01/15  22:50:47  tom
// (configure): don't terminate app if successful.
//
// Revision 1.4  1995/01/10  02:38:50  tom
// (appWillInit:): disable OK button if invalid euid;
// (panel:authenticateUser:withPassword:inDomain:): code cleanup.
//
// Revision 1.3  1995/01/04  21:50:54  tom
// (#import): add "version.h" <ansi.h> <sys/types.h> <sys/stat.h>
//  <nikit/NiLoginPanel.h>;
// (Private): replace _checkUserID with _checkUID:(int)uid, add NiLoginPanel
//  delegate method;
// (appWillInit:): new, check user IDs and eventually offer to install SUID news;
// (configure:): use _checkUID: method;
// (showInfoPanel:): fill in infoVersionField with actual version;
// (_checkUID): allowed users are "root", "news" or "usenet";
// (_runLoginPanel): fleshed out with NiLoginPanel.
//
// Revision 1.2  1994/12/27  23:49:03  tom
// (_command:name:): new support method to replace ad-hoc command-line building;
// (appDidInit,configure): use it;
// (appDidInit): look for $NEWSCONFIG in some semi-standard locations;
// (_runLoginPanel): enforce advertised behaviour.
//
// Revision 1.1  1994/12/20  14:52:20  tom
// Initial revision
//
//======================================================================
