//
//  FSAApp.m
//  F-Script Anywhere
//
//  Created by Nicholas Riley on Fri Feb 01 2002.
//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
//

/*

 F-Script Anywhere 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.

 F-Script Anywhere 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 F-Script Anywhere; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*/

#import "FSAApp.h"
#import "FSAnywhere.h"
#import <DSCL/PathManager.h>
#import <CoreFoundation/CoreFoundation.h>
#import <ApplicationServices/ApplicationServices.h>
#import <Security/AuthorizationTags.h>
#include <mach/mach.h>

NSString * const PatchBundleIdentifier = @"net.sabi.FScriptAnywhere";
NSString * const PatchBundleName = @"F-Script Anywhere.bundle";

typedef struct {
    OSStatus err;
    NSString * const desc;
} errRec, errList[];

// These are just educated guesses based on the errors I've seen; I haven't seen these documented anywhere!
static errList ERRS = { // XXX change for mach_inject/SCPatch
    { 5, @"F-Script Anywhere must be installed in a Cocoa application running as the current user.\n\nYou may be attempting to install in a setuid application, which is not supported" },
    { 11, @"F-Script Anywhere cannot be installed in itself.\n\nIf you wish to install F-Script Anywhere in itself, make a copy of the F-Script Anywhere application and install one copy into the other" },
    { smUnExBusErr, @"a bus error occurred.\n\nTry switching to the application first then using F-Script AnywhereÕs dock menu to install" },
    { fnfErr, @"F-Script Anywhere was unable to locate its component to install in the application.  Please try reinstalling F-Script Anywhere" },
    { cfragDupRegistrationErr, @"another running copy of F-Script Anywhere is already installed in the application"},
    { 0, nil }
};

NSMutableDictionary *FSA_errors;

NSString * FSA_descriptionForOSStatus(OSStatus err)
{
    NSString *desc = nil;
    
    if (FSA_errors != nil)
        desc = [FSA_errors objectForKey: [NSNumber numberWithLong: err]];
    
    if (desc != nil)
        return desc;
    
    return [NSString stringWithFormat: NSLocalizedString(@"an error of type %ld occurred", "Unknown error message used after 'XXX happened because'"), err];
}

FSAPatchController::FSAPatchController(FSAApp *fsaApp)
{
    mApp = fsaApp;
}

void FSAPatchController::PatchNotification(ProcessSerialNumber *inPSN, OSType inCreator, OSType type, CFStringRef name, UInt32 flags) {
    pid_t pid;
    OSStatus err = GetProcessPID(inPSN, &pid);
    
    NSCAssert3(err == noErr, @"PatchNotification can't get PID for PSN %ld.%ld: error %ld", inPSN->highLongOfPSN, inPSN->lowLongOfPSN, err);
    
    FSALog(@"Got patch notification for %@ (pid %d)", (NSString *)name, pid);
    [mApp controllerIsPatchingApplicationWithProcessID: pid];
}

// XXX should remove?
void FSAPatchController::ReceiveMessage(const AppleEvent *theAE) {
    OSErr err = noErr;
    Size actualSize;
    DescType actualType;
    OSType eventClass, eventID;
    
    if ((err = AEGetAttributePtr(theAE, keyEventClassAttr, typeType, &actualType, &eventClass, sizeof(OSType), &actualSize)) != noErr)
        return;
    if (eventClass != kSCMessageClass)
        return;

    if ((err = AEGetAttributePtr(theAE, keyEventIDAttr, typeType, &actualType, &eventID, sizeof(OSType), &actualSize)) != noErr)
        return;
 
#if 0
    if (eventID == kSCLoadResult) {
        OSStatus 
        (err = AEGetParamPtr(theAE, keyError, typeInteger, &actualType, &error, sizeof(long), &actualSize)) == noErr &&
        (err = AEGetParamPtr(theAE, keyBundleID, typeText, &actualType, bundleID, sizeof(bundleID), &actualSize)) == noErr)
        bundleID[actualSize] = 0;
        printf("patch bundle %s loaded with error status %d\n", bundleID, error);
    }
#endif
    

}


@implementation FSAApp

+ (void)initialize;
{
    errRec *rec;

    FSA_errors = [[NSMutableDictionary alloc] init];
    for (rec = &(ERRS[0]) ; rec->err != 0 ; rec++)
        [FSA_errors setObject: rec->desc forKey: [NSNumber numberWithLong: rec->err]];
}

-(void)installFScriptAtPath:(NSString *)installPath
{
    OSStatus myStatus;
    
    if(installPath){
        if([installPath hasSuffix:@"FScript.framework"]){
            installPath = [installPath stringByDeletingLastPathComponent];
            NSLog(@"new ip %@", installPath);
        }
    }
    
    if(!installPath){
        switch([[NSAlert alertWithMessageText:@"Install F-Script"
                                defaultButton:@"Current User"
                              alternateButton:@"All Users"
                                  otherButton:nil
                    informativeTextWithFormat:@"No previous version of F-Script found. You can choose to install for just your current user (~/Library/Frameworks) or for all users (/Library/Frameworks)."] runModal]){
            case NSAlertDefaultReturn:  // Current user
                installPath = [@"~/Library/Frameworks" stringByExpandingTildeInPath];
                break;
            case NSAlertAlternateReturn: // All users
                installPath = @"/Library/Frameworks";
                break;
            default:
                break;
        }        
    }
    
    NSString *fromPath = [[NSBundle mainBundle] pathForResource:@"FScript" ofType:@"framework"];
    if(![[NSFileManager defaultManager] fileExistsAtPath:installPath]){
        if ([[NSFileManager defaultManager] isWritableFileAtPath:installPath]){
            [[NSFileManager defaultManager] createDirectoryAtPath:installPath
                                                       attributes:nil];
        } else {
            [self createAuthorization];
            NSString *myToolPath = @"/bin/mkdir";
            char *myArguments[] = { (char *)[installPath UTF8String], NULL };
            myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, [myToolPath UTF8String],
                                                          myFlags, myArguments,
                                                          NULL);
        }
    }
    if([[NSFileManager defaultManager] isWritableFileAtPath:installPath]){
        [[NSFileManager defaultManager] copyPath:fromPath toPath:[installPath stringByAppendingString:@"/FScript.framework"] handler:nil];
    } else {
        [self createAuthorization];
        NSString *myToolPath = @"/bin/cp";
        char *myArguments[] = { "-R", "-f", (char *)[fromPath UTF8String], (char *)[installPath UTF8String], NULL };
        myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, [myToolPath UTF8String],
                                                      myFlags, myArguments,
                                                      NULL);
    }    
}

- (BOOL)_shouldContinueAfterFrameworkErrorPanelReturnedWithResult:(int)result withFrameworkPath:(NSString *)frameworkPath
{
    switch (result) {
        case NSAlertOtherReturn:
            [self terminate: self];
            return false;
        case NSAlertAlternateReturn:
            return true;
        case NSAlertDefaultReturn:  /* Install from resources */
            [self installFScriptAtPath:frameworkPath];
            return true;
        default:
            [self terminate: self];
            return false;
    }
}

-(BOOL)createAuthorization
{
    if(myAuthorizationRef)
        return YES;
    
    OSStatus myStatus;
    myFlags = kAuthorizationFlagDefaults;
    
    myStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment,
                                   myFlags, &myAuthorizationRef);
    if(myStatus != errAuthorizationSuccess)
        return NO;
    
    AuthorizationItem myItems = {kAuthorizationRightExecute, 0, NULL, 0};
    AuthorizationRights myRights = {1, &myItems};
    
    myFlags = kAuthorizationFlagDefaults
        | kAuthorizationFlagInteractionAllowed
        | kAuthorizationFlagPreAuthorize
        | kAuthorizationFlagExtendRights;
    myStatus = AuthorizationCopyRights(myAuthorizationRef, &myRights,
                                       NULL, myFlags, NULL);
    
    if(myStatus != errAuthorizationSuccess) 
        return NO;
    
    myFlags = kAuthorizationFlagDefaults;
    return YES;
}

-(tDirStatus)authorizeAndAddToProcMod:(NSString *)username
{
    OSStatus myStatus;
    
    if(![self createAuthorization])
        return eDSAuthFailed;
    
    NSString *myToolPath = [[NSBundle mainBundle] pathForResource:@"AddToProcMod" ofType:@""];
    char *myArguments[] = { (char *)[username UTF8String], NULL };
    FILE *myCommunicationsPipe = NULL;
    char myReadBuffer[128];
    myStatus = AuthorizationExecuteWithPrivileges(myAuthorizationRef, [myToolPath UTF8String],
                                                  myFlags, myArguments,
                                                  &myCommunicationsPipe);
    
    int didRead = 0;
    int lastRead;
    while((lastRead = read(fileno(myCommunicationsPipe), myReadBuffer, sizeof(myReadBuffer) - didRead - 1)) && (lastRead > 0))
        didRead += lastRead;
    
    myReadBuffer[didRead - 1] = 0;
    
    return (tDirStatus)strtol(myReadBuffer, NULL, 10);
}

- (void)finishLaunching
{
    mach_port_t     taskOfOurProcess = mach_task_self();
    mach_port_t     machPortForProcess;
    
    if(task_for_pid(taskOfOurProcess, 1, &machPortForProcess) != KERN_SUCCESS){ // launchd should always be pid 1
        if(![[NSUserDefaults standardUserDefaults] objectForKey:@"doPathCheck"])
            [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"doPathCheck"];
        
        if([[NSUserDefaults standardUserDefaults] boolForKey:@"doPathCheck"]){
top:
            PathManager *pm = [[PathManager alloc] initWithLocalNode];
            [pm backupStack];
            @try {
                [pm cd:@"/Groups/procmod"];
            } @catch ( NSException *exception ) {
                goto next;  // this means we must be on ppc 10.4 or less.
            }
            CFDictionaryRef sessionInfoDict = CGSessionCopyCurrentDictionary();
            if(sessionInfoDict){
                CFStringRef shortUserName = (CFStringRef)CFDictionaryGetValue(sessionInfoDict, kCGSessionUserNameKey);
                if(![[[pm lastObject] readArray:@"GroupMembership"] containsObject:(NSString *)shortUserName]){
                    switch([[NSAlert alertWithMessageText:[NSString stringWithFormat:@"User %@ not in the procmod group", shortUserName]
                                            defaultButton:@"Add me"
                                          alternateButton:@"Disable checking"
                                              otherButton:@"Ignore message"
                                informativeTextWithFormat:@"F-Script Anywhere requires that you add yourself to the procmod "
                        "group in order for it to function properly. If you like, F-Script Anywhere can automatically add you "
                        "to the procmod group."] runModal]){
                        case NSAlertDefaultReturn:
                        {
                            tDirStatus status = [self authorizeAndAddToProcMod:(NSString *)shortUserName];
                            if(status != eDSNoErr){
                                [[NSAlert alertWithMessageText:@"Error adding to procmod group"
                                                 defaultButton:nil
                                               alternateButton:nil
                                                   otherButton:nil
                                     informativeTextWithFormat:@"There was an error (%@) adding you to the procmod group. ", [[NSClassFromString(@"DSoStatus") sharedInstance] stringForStatus:status]] runModal];
                            } else {
                                [[NSAlert alertWithMessageText:@"Adding user procmod group succeeded"
                                                 defaultButton:nil
                                               alternateButton:nil
                                                   otherButton:nil
                                     informativeTextWithFormat:@"You may have to wait a few minutes until the system updates its caches (or alternatively, reboot your machine) before things will work properly."] runModal];
                            }
                        }
                            [pm restoreStack];
                            [pm release];
                            goto top;
                        case NSAlertAlternateReturn:
                            [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"doPathCheck"];
                            break;
                        default:
                            break;
                    }            
                }
            }
            [pm restoreStack];
            [pm release];
        }
    } else {
        mach_port_deallocate(taskOfOurProcess, machPortForProcess);
    }
next:
    patchController = new FSAPatchController(self);
    patchController->AddPatch((CFStringRef)PatchBundleIdentifier, CFSTR("Contents/Resources/"),
                              (CFStringRef)PatchBundleName);
    
    NSEnumerator *e = [[appList cocoaAppProcessIDs] objectEnumerator];
    pid_t pid;
    ProcessSerialNumber psn;
    OSStatus err;
    
    while ( (pid = [[e nextObject] longValue]) != 0) {
        err = GetProcessForPID(pid, &psn);
        
        if (err != noErr) {
            NSLog(@"Can't get PSN for PID %ld", pid);
            [appList applicationQuitWithProcessID: pid];
        } else if (patchController->IsProcessPatched(&psn)) {
            [appList didPatchProcessID: pid];
        }
    }

    // yes, someone could move the framework while the app is running, but they deserve what they get.
    NSFileManager *fileMgr = [NSFileManager defaultManager];
    NSArray *libraryDirectories =
        NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSAllDomainsMask, TRUE);
    NSString *libPath;
    NSString *frameworkPath = NULL;
    CFBundleRef frameworkBundle = NULL;
    BOOL isDirectory;

    e = [libraryDirectories objectEnumerator];
    while ( (libPath = [e nextObject]) != nil) {
        frameworkPath = [[libPath stringByAppendingPathComponent: @"Frameworks"]
            stringByAppendingPathComponent: @"FScript.framework"];
        if ([fileMgr fileExistsAtPath: frameworkPath isDirectory: &isDirectory] && isDirectory) {
            frameworkBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)[NSURL fileURLWithPath: frameworkPath]);
            [(id)frameworkBundle autorelease];
            break;
        }
    }
    FSALog(@"Framework bundle");
    FSAShow(frameworkBundle);
    [super finishLaunching];
    if (frameworkBundle == NULL) {
        int result = NSRunInformationalAlertPanel(
            NSLocalizedString(@"F-Script Framework Not Found", "Framework not found alert title"),
            NSLocalizedString(@"F-Script Anywhere requires the F-Script framework be installed in a Frameworks directory, such as ~/Library/Frameworks or /Library/Frameworks.\n\nTo download F-Script, please visit its Web site\n%@.\n\nThis package also includes a copy of F-Script for your convenience that can be installed easily by clicking below. Note that there may be a more recent version of F-Script available from the F-Script web site.\n\nIf you believe this message is in error, click '%@'.",
                              "Framework not found alert message, F-Script Web site URL parameter, 'Continue' button parameter"),
            NSLocalizedString(@"Install F-Script", "'Get F-Script' button title"),
            NSLocalizedString(@"Continue", "Continue button title"),
            NSLocalizedString(@"Quit", "Quit button title"),
            FSA_FScriptURL, NSLocalizedString(@"Continue", ""));
        if (![self _shouldContinueAfterFrameworkErrorPanelReturnedWithResult: result withFrameworkPath:nil]) return;
    } else {
        CFBundleRef includedBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"FScript" ofType:@"framework"]]);
        [(id)includedBundle autorelease];
        UInt32 includedVersion = CFBundleGetVersionNumber(includedBundle);
        UInt32 version = CFBundleGetVersionNumber(frameworkBundle);
        FSALog(@"Framework version 0x%x", version);
        if(version < includedVersion){
            if (version < FSA_FSCRIPT_MIN_VERSION){ // must force upgrade 
                NSString *frameworkVersion = (NSString *)CFBundleGetValueForInfoDictionaryKey(frameworkBundle, kCFBundleVersionKey);
                if (frameworkVersion == nil)
                    frameworkVersion = NSLocalizedString(@"unavailable", "Framework too old 'unavailable' version");
                int result = NSRunInformationalAlertPanel(
                                                          NSLocalizedString(@"F-Script Framework Too Old", "Framework too old alert title"),
                                                          NSLocalizedString(@"F-Script Anywhere %s requires version %@ or later of the F-Script framework be installed in a Frameworks directory, such as ~/Library/Frameworks or /Library/Frameworks. The installed version is %@.\n\nTo upgrade F-Script to the latest version, please visit its Web site\n%@.\n\nThis package also includes a copy of F-Script for your convenience that can be installed easily by clicking below. Note that there may be a more recent version of F-Script available from the F-Script web site.\n\nTo use this version of F-Script regardless of the problems you may experience, click '%@'.",
                                                                            "Framework too old alert message, FSA version parameter, minimum F-Script version parameter, current F-Script version parameter, F-Script Web site URL parameter, 'Continue' button parameter"),
                                                          NSLocalizedString(@"Install F-Script", "'Get F-Script' button title"),
                                                          NSLocalizedString(@"Continue", "Continue button title"),
                                                          NSLocalizedString(@"Quit", "Quit button title"),
                                                          FSA_VERSION, FSA_FScriptMinimumVersion, frameworkVersion,
                                                          FSA_FScriptURL, NSLocalizedString(@"Install F-Script", ""));
                if (![self _shouldContinueAfterFrameworkErrorPanelReturnedWithResult: result withFrameworkPath:frameworkPath]) return;                
            } else {
                NSNumber *lastChecked = [[NSUserDefaults standardUserDefaults] objectForKey:@"LastCheckedVersion"];
                if((version < includedVersion) && (!lastChecked || ([lastChecked unsignedIntValue] != version))){
                    FSALog(@"Included framework in resources is newer than installed version.");
                    int result = NSRunInformationalAlertPanel(
                                                              NSLocalizedString(@"Newer Version of F-Script Framework Available", "Framework newer found alert title"),
                                                              NSLocalizedString(@"F-Script Anywhere is bundled with a newer version of the F-Script framework. If you wish, F-Script Anywhere can upgrade your framework version.",
                                                                                "Framework newer found alert message, 'Continue' button parameter"),
                                                              NSLocalizedString(@"Upgrade F-Script Framework", "Upgrade button title"),
                                                              NSLocalizedString(@"Continue", "Continue button title"),
                                                              nil,
                                                              NSLocalizedString(@"Upgrade", ""), NSLocalizedString(@"Continue", ""));
                    if (![self _shouldContinueAfterFrameworkErrorPanelReturnedWithResult: result withFrameworkPath:frameworkPath]) return;                
                }
            }
        }
        [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithUnsignedInt:version] forKey:@"LastCheckedVersion"];
    }

    if(myAuthorizationRef){
        AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
        myAuthorizationRef = nil;
    }
    
    NSNotificationCenter *workspaceNotificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
    [workspaceNotificationCenter
        addObserver: self
           selector: @selector(applicationDidLaunch:)
               name: NSWorkspaceWillLaunchApplicationNotification
             object: nil];
    [workspaceNotificationCenter
        addObserver: self
           selector: @selector(applicationDidTerminate:)
               name: NSWorkspaceDidTerminateApplicationNotification
             object: nil];
    
    [appListWindow makeKeyAndOrderFront: self];
}

- (void)installationError:(OSStatus)err inAppWithPID:(pid_t)pid;
{
    NSBeginAlertSheet(NSLocalizedString(@"Installation failed", "Unable to install alert title"),
                      nil, nil, nil, appListWindow, self, nil, nil, nil,
                      NSLocalizedString(@"F-Script Anywhere was unable to install itself in the selected application (process ID %d), because %@.\n\nThe application may have crashed; restart it if needed.", "Unable to install alert message, PID parameter, 'XXX happened because' explanation parameter"),
                      pid, FSA_descriptionForOSStatus(err));
}

- (void)installBundleInAppWithPID:(pid_t)pid;
{
    if (pid == -1) return;
        
    ProcessSerialNumber psn;
    OSStatus err = GetProcessForPID(pid, &psn);
    
    if (err == noErr)
        err = patchController->PatchProcess(&psn);

    if (err != noErr)
        [self installationError: err inAppWithPID: pid];
    else
        [appList didPatchProcessID: pid];
}

- (void)loaderBundleMessage:(NSNotification *)notification;
{
    /*
    if (error) {
        [self installationError: error inAppWithPID: pid];
    } else {
        [appList didPatchProcessID: pid];
    } */
}

- (IBAction)installBundleInSelectedApp:(id)sender;
{
    [self installBundleInAppWithPID: [appList selectedProcessID]];
}

// from dock menu only!
- (IBAction)installBundleInFrontmostApp:(id)sender;
{
    NSAssert1([sender tag] > 0, @"Unable to determine frontmost application from %@", sender);
    [self installBundleInAppWithPID: (pid_t)[sender tag]];
}

@end

@implementation FSAApp (FSAPatchControllerDelegate)

- (void)controllerIsPatchingApplicationWithProcessID:(pid_t)pid;
{
    [appList isPatchingProcessID: pid];
}

@end

@implementation FSAApp (NSWorkspaceNotifications)

- (void)applicationDidLaunch:(NSNotification *)notification;
{
    [appList applicationLaunchedWithProcessID: [[[notification userInfo] objectForKey: @"NSApplicationProcessIdentifier"] intValue]];
}

- (void)applicationDidTerminate:(NSNotification *)notification;
{
    [appList applicationQuitWithProcessID: [[[notification userInfo] objectForKey: @"NSApplicationProcessIdentifier"] intValue]];
}

@end
