//
//  NSFileManager+Authentication.m
//  Sparkle
//
//  Created by Andy Matuschak on 3/9/06.
//  Copyright 2006 Andy Matuschak. All rights reserved.
//

// This code based on generous contribution from Allan Odgaard. Thanks, Allan!

#import "sys/stat.h"
#import <Security/Security.h>

#import <unistd.h>
#import <sys/stat.h>
#import <dirent.h>
#include <sys/types.h>
#include <sys/wait.h>

#import "NSFileManager+ExtendedAttributes.h"

// TN OV02 says that the range between 1000 and 9999 can be used for
// application-defined errors.  errToolFailedError will indicate that an
// executed program crashed or indicated failure with a nonzero exit status.
static const OSStatus errToolFailedError = 1000;

static OSStatus AuthorizationExecuteWithPrivilegesAndWait(
    AuthorizationRef authorization,
    const char* executablePath,
    AuthorizationFlags options,
    const char* const* arguments) {
	sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL);
	OSStatus ret;
	ret = AuthorizationExecuteWithPrivileges(authorization,
	                                         executablePath,
	                                         options,
	                                         (char* const*)arguments,
	                                         NULL);
	if (ret == errAuthorizationSuccess) {
		int status;
		pid_t pid = wait(&status);
		if (pid == -1 ||
		    !WIFEXITED(status) || WEXITSTATUS(status) != 0) {
			ret = errToolFailedError;
		}
	}
	signal(SIGCHLD, oldSigChildHandler);
	return ret;
}

@implementation NSFileManager (SUAuthenticationAdditions)

- (BOOL)currentUserOwnsPath:(NSString *)oPath
{
	char *path = (char *)[oPath fileSystemRepresentation];
	unsigned int uid = getuid();
	bool res = false;
	struct stat sb;
	if(stat(path, &sb) == 0)
	{
		if(sb.st_uid == uid)
		{
			res = true;
			if(sb.st_mode & S_IFDIR)
			{
				DIR* dir = opendir(path);
				struct dirent* entry = NULL;
				while(res && (entry = readdir(dir)))
				{
					if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
						continue;
					
					char descend[strlen(path) + 1 + entry->d_namlen + 1];
					strcpy(descend, path);
					strcat(descend, "/");
					strcat(descend, entry->d_name);
					res = [self currentUserOwnsPath:[NSString stringWithUTF8String:descend]];
				}
				closedir(dir);
			}
		}
	}
	return res;
}

- (BOOL)_movePathWithForcedAuthentication:(NSString *)src toPath:(NSString *)dst
{
	NSString *tmp = [[[dst stringByDeletingPathExtension] stringByAppendingString:@".old"] stringByAppendingPathExtension:[dst pathExtension]];

	const char* srcPath = [src fileSystemRepresentation];
	const char* tmpPath = [tmp fileSystemRepresentation];
	const char* dstPath = [dst fileSystemRepresentation];

	struct stat sb, dstSB;
	if ((stat(srcPath, &sb) != 0) ||
	    (stat(tmpPath, &sb) == 0) ||
	    (stat(dstPath, &dstSB) != 0)) {
		return NO;
	}

	AuthorizationRef auth;
	OSStatus authStat = errAuthorizationDenied;
	while (authStat == errAuthorizationDenied) {
		authStat = AuthorizationCreate(NULL,
		                               kAuthorizationEmptyEnvironment,
		                               kAuthorizationFlagDefaults,
		                               &auth);
	}

	BOOL res = NO;
	if (authStat == errAuthorizationSuccess) {
		res = YES;

		char uidgid[42];
		snprintf(uidgid, sizeof(uidgid), "%d:%d",
		         dstSB.st_uid, dstSB.st_gid);

		const char* executables[] = {
	  		"/bin/mv",
			"/bin/mv",
			"/bin/rm",
			NULL,  // pause here and do some housekeeping before
			       // continuing
			"/usr/sbin/chown",
			NULL   // stop here for real
		};

		// 4 is the maximum number of arguments to any command,
		// including the NULL that signals the end of an argument
		// list.
		const char* const argumentLists[][4] = {
			{ "-f", dstPath, tmpPath, NULL },  // mv
			{ "-f", srcPath, dstPath, NULL },  // mv
			{ "-rf", tmpPath, NULL },  // rm
			{ NULL },  // pause
			{ "-R", uidgid, dstPath, NULL },  // chown
			{ NULL }  // stop
		};

		// Process the commands up until the first NULL
		int commandIndex = 0;
		for (; executables[commandIndex] != NULL; ++commandIndex) {
			if (res) {
				res = (
				    AuthorizationExecuteWithPrivilegesAndWait(
				        auth,
				        executables[commandIndex],
				        kAuthorizationFlagDefaults,
				        argumentLists[commandIndex]) ==
				    errAuthorizationSuccess);
			}
		}

		// If the currently-running application is trusted, the new
		// version should be trusted as well.  Remove it from the
		// quarantine to avoid a delay at launch, and to avoid
		// presenting the user with a confusing trust dialog.
		//
		// This needs to be done after the application is moved to its
		// new home with "mv" in case it's moved across filesystems: if
		// that happens, "mv" actually performs a copy and may result
		// in the application being quarantined.  It also needs to be
		// done before "chown" changes ownership, because the ownership
		// change will almost certainly make it impossible to change
		// attributes to release the files from the quarantine.
		if (res) {
			[self releaseFromQuarantine:dst];
		}

		// Now move past the NULL we found and continue executing
		// commands from the list.
		++commandIndex;

		for (; executables[commandIndex] != NULL; ++commandIndex) {
			if (res) {
				res = (
				    AuthorizationExecuteWithPrivilegesAndWait(
				        auth,
				        executables[commandIndex],
				        kAuthorizationFlagDefaults,
				        argumentLists[commandIndex]) ==
				    errAuthorizationSuccess);
			}
		}

		AuthorizationFree(auth, 0);
	}

	return res;
}

- (BOOL)movePathWithAuthentication:(NSString *)src toPath:(NSString *)dst
{
	if ([[NSFileManager defaultManager] isWritableFileAtPath:dst] && [[NSFileManager defaultManager] isWritableFileAtPath:[dst stringByDeletingLastPathComponent]])
	{
		int tag = 0;
		BOOL result = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[dst stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[dst lastPathComponent]] tag:&tag];
		result &= [[NSFileManager defaultManager] movePath:src toPath:dst handler:NULL];

		// If the currently-running application is trusted, the new
		// version should be trusted as well.  Remove it from the
		// quarantine to avoid a delay at launch, and to avoid
		// presenting the user with a confusing trust dialog.
		//
		// This needs to be done after the application is moved to its
		// new home in case it's moved across filesystems: if that
		// happens, the move is actually a copy, and it may result
		// in the application being quarantined.
		if (result) {
			[self releaseFromQuarantine:dst];
		}

		return result;
	}
	else
	{
		return [self _movePathWithForcedAuthentication:src toPath:dst];
	}
}

@end
