source: trunk/Sparkle/SUPlainInstallerInternals.m @ 1046

Revision 128, 12.3 KB checked in by speck, 4 years ago (diff)

Fixed missing display of update notes in Sparkle. Embedded Sparkle so subversion has the proper version instead of having to download Sparkle and apply several patches.

Line 
1//
2//  SUPlainInstallerInternals.m
3//  Sparkle
4//
5//  Created by Andy Matuschak on 3/9/06.
6//  Copyright 2006 Andy Matuschak. All rights reserved.
7//
8
9#import "Sparkle.h"
10#import "SUPlainInstallerInternals.h"
11
12#import <Security/Security.h>
13#import <sys/stat.h>
14#import <sys/wait.h>
15#import <dirent.h>
16#import <unistd.h>
17
18@interface SUPlainInstaller (MMExtendedAttributes)
19// Removes the directory tree rooted at |root| from the file quarantine.
20// The quarantine was introduced on Mac OS X 10.5 and is described at:
21//
22//   http://developer.apple.com/releasenotes/Carbon/RN-LaunchServices/index.html
23//#apple_ref/doc/uid/TP40001369-DontLinkElementID_2
24//
25// If |root| is not a directory, then it alone is removed from the quarantine.
26// Symbolic links, including |root| if it is a symbolic link, will not be
27// traversed.
28//
29// Ordinarily, the quarantine is managed by calling LSSetItemAttribute
30// to set the kLSItemQuarantineProperties attribute to a dictionary specifying
31// the quarantine properties to be applied.  However, it does not appear to be
32// possible to remove an item from the quarantine directly through any public
33// Launch Services calls.  Instead, this method takes advantage of the fact
34// that the quarantine is implemented in part by setting an extended attribute,
35// "com.apple.quarantine", on affected files.  Removing this attribute is
36// sufficient to remove files from the quarantine.
37+ (void)releaseFromQuarantine:(NSString*)root;
38@end
39
40// Authorization code based on generous contribution from Allan Odgaard. Thanks, Allan!
41
42static BOOL AuthorizationExecuteWithPrivilegesAndWait(AuthorizationRef authorization, const char* executablePath, AuthorizationFlags options, const char* const* arguments)
43{
44    sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL);
45    BOOL returnValue = YES;
46
47    if (AuthorizationExecuteWithPrivileges(authorization, executablePath, options, (char* const*)arguments, NULL) == errAuthorizationSuccess)
48    {
49        int status;
50        pid_t pid = wait(&status);
51        if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0)
52            returnValue = NO;
53    }
54    else
55        returnValue = NO;
56       
57    signal(SIGCHLD, oldSigChildHandler);
58    return returnValue;
59}
60
61@implementation SUPlainInstaller (Internals)
62
63+ (BOOL)currentUserOwnsPath:(NSString *)oPath
64{
65    const char *path = [oPath fileSystemRepresentation];
66    uid_t uid = getuid();
67    bool res = false;
68    struct stat sb;
69    if(stat(path, &sb) == 0)
70    {
71        if(sb.st_uid == uid)
72        {
73            res = true;
74            if(sb.st_mode & S_IFDIR)
75            {
76                DIR* dir = opendir(path);
77                struct dirent* entry = NULL;
78                while(res && (entry = readdir(dir)))
79                {
80                    if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
81                        continue;
82                   
83                    size_t len = strlen(path) + 1 + entry->d_namlen + 1;
84                    char descend[len];
85                    strlcpy(descend, path, len);
86                    strlcat(descend, "/", len);
87                    strlcat(descend, entry->d_name, len);
88                    NSString* newPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:descend length:strlen(descend)];
89                    res = [self currentUserOwnsPath:newPath];
90                }
91                closedir(dir);
92            }
93        }
94    }
95    return res;
96}
97
98+ (NSString *)_temporaryCopyNameForPath:(NSString *)path
99{
100    // Let's try to read the version number so the filename will be more meaningful.
101    NSString *postFix;
102    NSBundle *bundle;
103    if ((bundle = [NSBundle bundleWithPath:path]))
104    {
105        // We'll clean it up a little for safety.
106        // The cast is necessary because of a bug in the headers in pre-10.5 SDKs
107        NSMutableCharacterSet *validCharacters = (id)[NSMutableCharacterSet alphanumericCharacterSet];
108        [validCharacters formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@".-()"]];
109        postFix = [[bundle objectForInfoDictionaryKey:@"CFBundleVersion"] stringByTrimmingCharactersInSet:[validCharacters invertedSet]];
110    }
111    else
112        postFix = @"old";
113    NSString *prefix = [[path stringByDeletingPathExtension] stringByAppendingFormat:@" (%@)", postFix];
114    NSString *tempDir = [prefix stringByAppendingPathExtension:[path pathExtension]];
115    // Now let's make sure we get a unique path.
116    int cnt=2;
117    while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 999999)
118        tempDir = [NSString stringWithFormat:@"%@ %d.%@", prefix, cnt++, [path pathExtension]];
119    return tempDir;
120}
121
122+ (BOOL)_copyPathWithForcedAuthentication:(NSString *)src toPath:(NSString *)dst error:(NSError **)error
123{
124    NSString *tmp = [self _temporaryCopyNameForPath:dst];
125    const char* srcPath = [src fileSystemRepresentation];
126    const char* tmpPath = [tmp fileSystemRepresentation];
127    const char* dstPath = [dst fileSystemRepresentation];
128   
129    struct stat dstSB;
130    stat(dstPath, &dstSB);
131   
132    AuthorizationRef auth = NULL;
133    OSStatus authStat = errAuthorizationDenied;
134    while (authStat == errAuthorizationDenied) {
135        authStat = AuthorizationCreate(NULL,
136                                       kAuthorizationEmptyEnvironment,
137                                       kAuthorizationFlagDefaults,
138                                       &auth);
139    }
140   
141    BOOL res = NO;
142    if (authStat == errAuthorizationSuccess) {
143        res = YES;
144       
145        char uidgid[42];
146        snprintf(uidgid, sizeof(uidgid), "%d:%d",
147                 dstSB.st_uid, dstSB.st_gid);
148       
149        const char* executables[] = {
150            "/bin/rm",
151            "/bin/mv",
152            "/bin/mv",
153            "/bin/rm",
154            NULL,  // pause here and do some housekeeping before
155            // continuing
156            "/usr/sbin/chown",
157            NULL   // stop here for real
158        };
159       
160        // 4 is the maximum number of arguments to any command,
161        // including the NULL that signals the end of an argument
162        // list.
163        const char* const argumentLists[][4] = {
164            { "-rf", tmpPath, NULL }, // make room for the temporary file... this is kinda unsafe; should probably do something better.
165            { "-f", dstPath, tmpPath, NULL },  // mv
166            { "-f", srcPath, dstPath, NULL },  // mv
167            { "-rf", tmpPath, NULL },  // rm
168            { NULL },  // pause
169            { "-R", uidgid, dstPath, NULL },  // chown
170            { NULL }  // stop
171        };
172       
173        // Process the commands up until the first NULL
174        int commandIndex = 0;
175        for (; executables[commandIndex] != NULL; ++commandIndex) {
176            if (res)
177                res = AuthorizationExecuteWithPrivilegesAndWait(auth, executables[commandIndex], kAuthorizationFlagDefaults, argumentLists[commandIndex]);
178        }
179       
180        // If the currently-running application is trusted, the new
181        // version should be trusted as well.  Remove it from the
182        // quarantine to avoid a delay at launch, and to avoid
183        // presenting the user with a confusing trust dialog.
184        //
185        // This needs to be done after the application is moved to its
186        // new home with "mv" in case it's moved across filesystems: if
187        // that happens, "mv" actually performs a copy and may result
188        // in the application being quarantined.  It also needs to be
189        // done before "chown" changes ownership, because the ownership
190        // change will almost certainly make it impossible to change
191        // attributes to release the files from the quarantine.
192        if (res) {
193            [self releaseFromQuarantine:dst];
194        }
195       
196        // Now move past the NULL we found and continue executing
197        // commands from the list.
198        ++commandIndex;
199       
200        for (; executables[commandIndex] != NULL; ++commandIndex) {
201            if (res)
202                res = AuthorizationExecuteWithPrivilegesAndWait(auth, executables[commandIndex], kAuthorizationFlagDefaults, argumentLists[commandIndex]);
203        }
204       
205        AuthorizationFree(auth, 0);
206       
207        if (!res)
208        {
209            // Something went wrong somewhere along the way, but we're not sure exactly where.
210            NSString *errorMessage = [NSString stringWithFormat:@"Authenticated file copy from %@ to %@ failed.", src, dst];
211            if (error != NULL)
212                *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:[NSDictionary dictionaryWithObject:errorMessage forKey:NSLocalizedDescriptionKey]];
213        }
214    }
215    else
216    {
217        if (error != NULL)
218            *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:[NSDictionary dictionaryWithObject:@"Couldn't get permission to authenticate." forKey:NSLocalizedDescriptionKey]];
219    }
220    return res;
221}
222
223+ (BOOL)copyPathWithAuthentication:(NSString *)src overPath:(NSString *)dst error:(NSError **)error
224{
225    if (![[NSFileManager defaultManager] fileExistsAtPath:dst])
226    {
227        NSString *errorMessage = [NSString stringWithFormat:@"Couldn't copy %@ over %@ because there is no file at %@.", src, dst, dst];
228        if (error != NULL)
229            *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:errorMessage forKey:NSLocalizedDescriptionKey]];
230        return NO;
231    }
232
233    if (![[NSFileManager defaultManager] isWritableFileAtPath:dst] || ![[NSFileManager defaultManager] isWritableFileAtPath:[dst stringByDeletingLastPathComponent]])
234        return [self _copyPathWithForcedAuthentication:src toPath:dst error:error];
235
236    NSString *tmpPath = [self _temporaryCopyNameForPath:dst];
237
238    if (![[NSFileManager defaultManager] movePath:dst toPath:tmpPath handler:self])
239    {
240        if (error != NULL)
241            *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Couldn't move %@ to %@.", dst, tmpPath] forKey:NSLocalizedDescriptionKey]];
242        return NO;         
243    }
244    if (![[NSFileManager defaultManager] copyPath:src toPath:dst handler:self])
245    {
246        if (error != NULL)
247            *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Couldn't copy %@ to %@.", src, dst] forKey:NSLocalizedDescriptionKey]];
248        return NO;         
249    }
250   
251    // Trash the old copy of the app.
252    NSInteger tag = 0;
253    if (![[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[tmpPath stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[tmpPath lastPathComponent]] tag:&tag])
254        NSLog(@"Sparkle error: couldn't move %@ to the trash. This is often a sign of a permissions error.", tmpPath);
255   
256    // If the currently-running application is trusted, the new
257    // version should be trusted as well.  Remove it from the
258    // quarantine to avoid a delay at launch, and to avoid
259    // presenting the user with a confusing trust dialog.
260    //
261    // This needs to be done after the application is moved to its
262    // new home in case it's moved across filesystems: if that
263    // happens, the move is actually a copy, and it may result
264    // in the application being quarantined.
265    [self releaseFromQuarantine:dst];
266   
267    return YES;
268}
269
270@end
271
272#include <dlfcn.h>
273#include <errno.h>
274#include <sys/xattr.h>
275
276@implementation SUPlainInstaller (MMExtendedAttributes)
277
278+ (int)removeXAttr:(const char*)name
279          fromFile:(NSString*)file
280           options:(int)options
281{
282    typedef int (*removexattr_type)(const char*, const char*, int);
283    // Reference removexattr directly, it's in the SDK.
284    static removexattr_type removexattr_func = removexattr;
285   
286    // Make sure that the symbol is present.  This checks the deployment
287    // target instead of the SDK so that it's able to catch dlsym failures
288    // as well as the null symbol that would result from building with the
289    // 10.4 SDK and a lower deployment target, and running on 10.3.
290    if (!removexattr_func) {
291        errno = ENOSYS;
292        return -1;
293    }
294   
295    const char* path = NULL;
296    @try {
297        path = [file fileSystemRepresentation];
298    }
299    @catch (id exception) {
300        // -[NSString fileSystemRepresentation] throws an exception if it's
301        // unable to convert the string to something suitable.  Map that to
302        // EDOM, "argument out of domain", which sort of conveys that there
303        // was a conversion failure.
304        errno = EDOM;
305        return -1;
306    }
307   
308    return removexattr_func(path, name, options);
309}
310
311+ (void)releaseFromQuarantine:(NSString*)root
312{
313    const char* quarantineAttribute = "com.apple.quarantine";
314    const int removeXAttrOptions = XATTR_NOFOLLOW;
315   
316    [self removeXAttr:quarantineAttribute
317             fromFile:root
318              options:removeXAttrOptions];
319   
320    // Only recurse if it's actually a directory.  Don't recurse into a
321    // root-level symbolic link.
322    NSDictionary* rootAttributes =
323    [[NSFileManager defaultManager] fileAttributesAtPath:root traverseLink:NO];
324    NSString* rootType = [rootAttributes objectForKey:NSFileType];
325   
326    if (rootType == NSFileTypeDirectory) {
327        // The NSDirectoryEnumerator will avoid recursing into any contained
328        // symbolic links, so no further type checks are needed.
329        NSDirectoryEnumerator* directoryEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:root];
330        NSString* file = nil;
331        while ((file = [directoryEnumerator nextObject])) {
332            [self removeXAttr:quarantineAttribute
333                     fromFile:[root stringByAppendingPathComponent:file]
334                      options:removeXAttrOptions];
335        }
336    }
337}
338
339@end
Note: See TracBrowser for help on using the repository browser.