/thirdparty/breakpad/client/mac/sender/crash_report_sender.m
Objective C | 753 lines | 490 code | 115 blank | 148 comment | 77 complexity | c671d46572194b8816a59cbfcbda56fa MD5 | raw file
1// Copyright (c) 2006, Google Inc. 2// All rights reserved. 3// 4// Redistribution and use in source and binary forms, with or without 5// modification, are permitted provided that the following conditions are 6// met: 7// 8// * Redistributions of source code must retain the above copyright 9// notice, this list of conditions and the following disclaimer. 10// * Redistributions in binary form must reproduce the above 11// copyright notice, this list of conditions and the following disclaimer 12// in the documentation and/or other materials provided with the 13// distribution. 14// * Neither the name of Google Inc. nor the names of its 15// contributors may be used to endorse or promote products derived from 16// this software without specific prior written permission. 17// 18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30#import "client/mac/sender/crash_report_sender.h" 31 32#import <Cocoa/Cocoa.h> 33#import <pwd.h> 34#import <sys/stat.h> 35#import <SystemConfiguration/SystemConfiguration.h> 36#import <unistd.h> 37 38#import "client/apple/Framework/BreakpadDefines.h" 39#import "common/mac/GTMLogger.h" 40#import "common/mac/HTTPMultipartUpload.h" 41 42 43#define kLastSubmission @"LastSubmission" 44const int kUserCommentsMaxLength = 1500; 45const int kEmailMaxLength = 64; 46 47#define kApplePrefsSyncExcludeAllKey \ 48 @"com.apple.PreferenceSync.ExcludeAllSyncKeys" 49 50#pragma mark - 51 52@interface NSView (ResizabilityExtentions) 53// Shifts the view vertically by the given amount. 54- (void)breakpad_shiftVertically:(CGFloat)offset; 55 56// Shifts the view horizontally by the given amount. 57- (void)breakpad_shiftHorizontally:(CGFloat)offset; 58@end 59 60@implementation NSView (ResizabilityExtentions) 61- (void)breakpad_shiftVertically:(CGFloat)offset { 62 NSPoint origin = [self frame].origin; 63 origin.y += offset; 64 [self setFrameOrigin:origin]; 65} 66 67- (void)breakpad_shiftHorizontally:(CGFloat)offset { 68 NSPoint origin = [self frame].origin; 69 origin.x += offset; 70 [self setFrameOrigin:origin]; 71} 72@end 73 74@interface NSWindow (ResizabilityExtentions) 75// Adjusts the window height by heightDelta relative to its current height, 76// keeping all the content at the same size. 77- (void)breakpad_adjustHeight:(CGFloat)heightDelta; 78@end 79 80@implementation NSWindow (ResizabilityExtentions) 81- (void)breakpad_adjustHeight:(CGFloat)heightDelta { 82 [[self contentView] setAutoresizesSubviews:NO]; 83 84 NSRect windowFrame = [self frame]; 85 windowFrame.size.height += heightDelta; 86 [self setFrame:windowFrame display:YES]; 87 // For some reason the content view is resizing, but not adjusting its origin, 88 // so correct it manually. 89 [[self contentView] setFrameOrigin:NSMakePoint(0, 0)]; 90 91 [[self contentView] setAutoresizesSubviews:YES]; 92} 93@end 94 95@interface NSTextField (ResizabilityExtentions) 96// Grows or shrinks the height of the field to the minimum required to show the 97// current text, preserving the existing width and origin. 98// Returns the change in height. 99- (CGFloat)breakpad_adjustHeightToFit; 100 101// Grows or shrinks the width of the field to the minimum required to show the 102// current text, preserving the existing height and origin. 103// Returns the change in width. 104- (CGFloat)breakpad_adjustWidthToFit; 105@end 106 107@implementation NSTextField (ResizabilityExtentions) 108- (CGFloat)breakpad_adjustHeightToFit { 109 NSRect oldFrame = [self frame]; 110 // Starting with the 10.5 SDK, height won't grow, so make it huge to start. 111 NSRect presizeFrame = oldFrame; 112 presizeFrame.size.height = MAXFLOAT; 113 // sizeToFit will blow out the width rather than making the field taller, so 114 // we do it manually. 115 NSSize newSize = [[self cell] cellSizeForBounds:presizeFrame]; 116 NSRect newFrame = NSMakeRect(oldFrame.origin.x, oldFrame.origin.y, 117 NSWidth(oldFrame), newSize.height); 118 [self setFrame:newFrame]; 119 120 return newSize.height - NSHeight(oldFrame); 121} 122 123- (CGFloat)breakpad_adjustWidthToFit { 124 NSRect oldFrame = [self frame]; 125 [self sizeToFit]; 126 return NSWidth([self frame]) - NSWidth(oldFrame); 127} 128@end 129 130@interface NSButton (ResizabilityExtentions) 131// Resizes to fit the label using IB-style size-to-fit metrics and enforcing a 132// minimum width of 70, while preserving the right edge location. 133// Returns the change in width. 134- (CGFloat)breakpad_smartSizeToFit; 135@end 136 137@implementation NSButton (ResizabilityExtentions) 138- (CGFloat)breakpad_smartSizeToFit { 139 NSRect oldFrame = [self frame]; 140 [self sizeToFit]; 141 NSRect newFrame = [self frame]; 142 // sizeToFit gives much worse results that IB's Size to Fit option. This is 143 // the amount of padding IB adds over a sizeToFit, empirically determined. 144 const float kExtraPaddingAmount = 12; 145 const float kMinButtonWidth = 70; // The default button size in IB. 146 newFrame.size.width = NSWidth(newFrame) + kExtraPaddingAmount; 147 if (NSWidth(newFrame) < kMinButtonWidth) 148 newFrame.size.width = kMinButtonWidth; 149 // Preserve the right edge location. 150 newFrame.origin.x = NSMaxX(oldFrame) - NSWidth(newFrame); 151 [self setFrame:newFrame]; 152 return NSWidth(newFrame) - NSWidth(oldFrame); 153} 154@end 155 156#pragma mark - 157 158@interface Reporter(PrivateMethods) 159- (id)initWithConfigFile:(const char *)configFile; 160 161// Returns YES if it has been long enough since the last report that we should 162// submit a report for this crash. 163- (BOOL)reportIntervalElapsed; 164 165// Returns YES if we should send the report without asking the user first. 166- (BOOL)shouldSubmitSilently; 167 168// Returns YES if the minidump was generated on demand. 169- (BOOL)isOnDemand; 170 171// Returns YES if we should ask the user to provide comments. 172- (BOOL)shouldRequestComments; 173 174// Returns YES if we should ask the user to provide an email address. 175- (BOOL)shouldRequestEmail; 176 177// Shows UI to the user to ask for permission to send and any extra information 178// we've been instructed to request. Returns YES if the user allows the report 179// to be sent. 180- (BOOL)askUserPermissionToSend; 181 182// Returns the short description of the crash, suitable for use as a dialog 183// title (e.g., "The application Foo has quit unexpectedly"). 184- (NSString*)shortDialogMessage; 185 186// Return explanatory text about the crash and the reporter, suitable for the 187// body text of a dialog. 188- (NSString*)explanatoryDialogText; 189 190// Returns the amount of time the UI should be shown before timing out. 191- (NSTimeInterval)messageTimeout; 192 193// Preps the comment-prompting alert window for display: 194// * localizes all the elements 195// * resizes and adjusts layout as necessary for localization 196// * removes the email section if includeEmail is NO 197- (void)configureAlertWindowIncludingEmail:(BOOL)includeEmail; 198 199// Rmevoes the email section of the dialog, adjusting the rest of the window 200// as necessary. 201- (void)removeEmailPrompt; 202 203// Run an alert window with the given timeout. Returns 204// NSRunStoppedResponse if the timeout is exceeded. A timeout of 0 205// queues the message immediately in the modal run loop. 206- (NSInteger)runModalWindow:(NSWindow*)window 207 withTimeout:(NSTimeInterval)timeout; 208 209// This method is used to periodically update the UI with how many 210// seconds are left in the dialog display. 211- (void)updateSecondsLeftInDialogDisplay:(NSTimer*)theTimer; 212 213// When we receive this notification, it means that the user has 214// begun editing the email address or comments field, and we disable 215// the timers so that the user has as long as they want to type 216// in their comments/email. 217- (void)controlTextDidBeginEditing:(NSNotification *)aNotification; 218 219- (void)report; 220 221@end 222 223@implementation Reporter 224//============================================================================= 225- (id)initWithConfigFile:(const char *)configFile { 226 if ((self = [super init])) { 227 remainingDialogTime_ = 0; 228 uploader_ = [[Uploader alloc] initWithConfigFile:configFile]; 229 if (!uploader_) { 230 [self release]; 231 return nil; 232 } 233 } 234 return self; 235} 236 237//============================================================================= 238- (BOOL)askUserPermissionToSend { 239 // Initialize Cocoa, needed to display the alert 240 NSApplicationLoad(); 241 242 // Get the timeout value for the notification. 243 NSTimeInterval timeout = [self messageTimeout]; 244 245 NSInteger buttonPressed = NSAlertAlternateReturn; 246 // Determine whether we should create a text box for user feedback. 247 if ([self shouldRequestComments]) { 248 BOOL didLoadNib = [NSBundle loadNibNamed:@"Breakpad" owner:self]; 249 if (!didLoadNib) { 250 return NO; 251 } 252 253 [self configureAlertWindowIncludingEmail:[self shouldRequestEmail]]; 254 255 buttonPressed = [self runModalWindow:alertWindow_ withTimeout:timeout]; 256 257 // Extract info from the user into the uploader_. 258 if ([self commentsValue]) { 259 [[uploader_ parameters] setObject:[self commentsValue] 260 forKey:@BREAKPAD_COMMENTS]; 261 } 262 if ([self emailValue]) { 263 [[uploader_ parameters] setObject:[self emailValue] 264 forKey:@BREAKPAD_EMAIL]; 265 } 266 } else { 267 // Create an alert panel to tell the user something happened 268 NSPanel* alert = NSGetAlertPanel([self shortDialogMessage], 269 [self explanatoryDialogText], 270 NSLocalizedString(@"sendReportButton", @""), 271 NSLocalizedString(@"cancelButton", @""), 272 nil); 273 274 // Pop the alert with an automatic timeout, and wait for the response 275 buttonPressed = [self runModalWindow:alert withTimeout:timeout]; 276 277 // Release the panel memory 278 NSReleaseAlertPanel(alert); 279 } 280 return buttonPressed == NSAlertDefaultReturn; 281} 282 283- (void)configureAlertWindowIncludingEmail:(BOOL)includeEmail { 284 // Swap in localized values, making size adjustments to impacted elements as 285 // we go. Remember that the origin is in the bottom left, so elements above 286 // "fall" as text areas are shrunk from their overly-large IB sizes. 287 288 // Localize the header. No resizing needed, as it has plenty of room. 289 [dialogTitle_ setStringValue:[self shortDialogMessage]]; 290 291 // Localize the explanatory text field. 292 [commentMessage_ setStringValue:[NSString stringWithFormat:@"%@\n\n%@", 293 [self explanatoryDialogText], 294 NSLocalizedString(@"commentsMsg", @"")]]; 295 CGFloat commentHeightDelta = [commentMessage_ breakpad_adjustHeightToFit]; 296 [headerBox_ breakpad_shiftVertically:commentHeightDelta]; 297 [alertWindow_ breakpad_adjustHeight:commentHeightDelta]; 298 299 // Either localize the email explanation field or remove the whole email 300 // section depending on whether or not we are asking for email. 301 if (includeEmail) { 302 [emailMessage_ setStringValue:NSLocalizedString(@"emailMsg", @"")]; 303 CGFloat emailHeightDelta = [emailMessage_ breakpad_adjustHeightToFit]; 304 [preEmailBox_ breakpad_shiftVertically:emailHeightDelta]; 305 [alertWindow_ breakpad_adjustHeight:emailHeightDelta]; 306 } else { 307 [self removeEmailPrompt]; // Handles necessary resizing. 308 } 309 310 // Localize the email label, and shift the associated text field. 311 [emailLabel_ setStringValue:NSLocalizedString(@"emailLabel", @"")]; 312 CGFloat emailLabelWidthDelta = [emailLabel_ breakpad_adjustWidthToFit]; 313 [emailEntryField_ breakpad_shiftHorizontally:emailLabelWidthDelta]; 314 315 // Localize the privacy policy label, and keep it right-aligned to the arrow. 316 [privacyLinkLabel_ setStringValue:NSLocalizedString(@"privacyLabel", @"")]; 317 CGFloat privacyLabelWidthDelta = 318 [privacyLinkLabel_ breakpad_adjustWidthToFit]; 319 [privacyLinkLabel_ breakpad_shiftHorizontally:(-privacyLabelWidthDelta)]; 320 321 // Ensure that the email field and the privacy policy link don't overlap. 322 CGFloat kMinControlPadding = 8; 323 CGFloat maxEmailFieldWidth = NSMinX([privacyLinkLabel_ frame]) - 324 NSMinX([emailEntryField_ frame]) - 325 kMinControlPadding; 326 if (NSWidth([emailEntryField_ bounds]) > maxEmailFieldWidth && 327 maxEmailFieldWidth > 0) { 328 NSSize emailSize = [emailEntryField_ frame].size; 329 emailSize.width = maxEmailFieldWidth; 330 [emailEntryField_ setFrameSize:emailSize]; 331 } 332 333 // Localize the placeholder text. 334 [[commentsEntryField_ cell] 335 setPlaceholderString:NSLocalizedString(@"commentsPlaceholder", @"")]; 336 [[emailEntryField_ cell] 337 setPlaceholderString:NSLocalizedString(@"emailPlaceholder", @"")]; 338 339 // Localize the buttons, and keep the cancel button at the right distance. 340 [sendButton_ setTitle:NSLocalizedString(@"sendReportButton", @"")]; 341 CGFloat sendButtonWidthDelta = [sendButton_ breakpad_smartSizeToFit]; 342 [cancelButton_ breakpad_shiftHorizontally:(-sendButtonWidthDelta)]; 343 [cancelButton_ setTitle:NSLocalizedString(@"cancelButton", @"")]; 344 [cancelButton_ breakpad_smartSizeToFit]; 345} 346 347- (void)removeEmailPrompt { 348 [emailSectionBox_ setHidden:YES]; 349 CGFloat emailSectionHeight = NSHeight([emailSectionBox_ frame]); 350 [preEmailBox_ breakpad_shiftVertically:(-emailSectionHeight)]; 351 [alertWindow_ breakpad_adjustHeight:(-emailSectionHeight)]; 352} 353 354- (NSInteger)runModalWindow:(NSWindow*)window 355 withTimeout:(NSTimeInterval)timeout { 356 // Queue a |stopModal| message to be performed in |timeout| seconds. 357 if (timeout > 0.001) { 358 remainingDialogTime_ = timeout; 359 SEL updateSelector = @selector(updateSecondsLeftInDialogDisplay:); 360 messageTimer_ = [NSTimer scheduledTimerWithTimeInterval:1.0 361 target:self 362 selector:updateSelector 363 userInfo:nil 364 repeats:YES]; 365 } 366 367 // Run the window modally and wait for either a |stopModal| message or a 368 // button click. 369 [NSApp activateIgnoringOtherApps:YES]; 370 NSInteger returnMethod = [NSApp runModalForWindow:window]; 371 372 return returnMethod; 373} 374 375- (IBAction)sendReport:(id)sender { 376 // Force the text fields to end editing so text for the currently focused 377 // field will be commited. 378 [alertWindow_ makeFirstResponder:alertWindow_]; 379 380 [alertWindow_ orderOut:self]; 381 // Use NSAlertDefaultReturn so that the return value of |runModalWithWindow| 382 // matches the AppKit function NSRunAlertPanel() 383 [NSApp stopModalWithCode:NSAlertDefaultReturn]; 384} 385 386// UI Button Actions 387//============================================================================= 388- (IBAction)cancel:(id)sender { 389 [alertWindow_ orderOut:self]; 390 // Use NSAlertDefaultReturn so that the return value of |runModalWithWindow| 391 // matches the AppKit function NSRunAlertPanel() 392 [NSApp stopModalWithCode:NSAlertAlternateReturn]; 393} 394 395- (IBAction)showPrivacyPolicy:(id)sender { 396 // Get the localized privacy policy URL and open it in the default browser. 397 NSURL* privacyPolicyURL = 398 [NSURL URLWithString:NSLocalizedString(@"privacyPolicyURL", @"")]; 399 [[NSWorkspace sharedWorkspace] openURL:privacyPolicyURL]; 400} 401 402// Text Field Delegate Methods 403//============================================================================= 404- (BOOL) control:(NSControl*)control 405 textView:(NSTextView*)textView 406doCommandBySelector:(SEL)commandSelector { 407 BOOL result = NO; 408 // If the user has entered text on the comment field, don't end 409 // editing on "return". 410 if (control == commentsEntryField_ && 411 commandSelector == @selector(insertNewline:) 412 && [[textView string] length] > 0) { 413 [textView insertNewlineIgnoringFieldEditor:self]; 414 result = YES; 415 } 416 return result; 417} 418 419- (void)controlTextDidBeginEditing:(NSNotification *)aNotification { 420 [messageTimer_ invalidate]; 421 [self setCountdownMessage:@""]; 422} 423 424- (void)updateSecondsLeftInDialogDisplay:(NSTimer*)theTimer { 425 remainingDialogTime_ -= 1; 426 427 NSString *countdownMessage; 428 NSString *formatString; 429 430 int displayedTimeLeft; // This can be either minutes or seconds. 431 432 if (remainingDialogTime_ > 59) { 433 // calculate minutes remaining for UI purposes 434 displayedTimeLeft = (int)(remainingDialogTime_ / 60); 435 436 if (displayedTimeLeft == 1) { 437 formatString = NSLocalizedString(@"countdownMsgMinuteSingular", @""); 438 } else { 439 formatString = NSLocalizedString(@"countdownMsgMinutesPlural", @""); 440 } 441 } else { 442 displayedTimeLeft = (int)remainingDialogTime_; 443 if (displayedTimeLeft == 1) { 444 formatString = NSLocalizedString(@"countdownMsgSecondSingular", @""); 445 } else { 446 formatString = NSLocalizedString(@"countdownMsgSecondsPlural", @""); 447 } 448 } 449 countdownMessage = [NSString stringWithFormat:formatString, 450 displayedTimeLeft]; 451 if (remainingDialogTime_ <= 30) { 452 [countdownLabel_ setTextColor:[NSColor redColor]]; 453 } 454 [self setCountdownMessage:countdownMessage]; 455 if (remainingDialogTime_ <= 0) { 456 [messageTimer_ invalidate]; 457 [NSApp stopModal]; 458 } 459} 460 461 462 463#pragma mark Accessors 464#pragma mark - 465//============================================================================= 466 467- (NSString *)commentsValue { 468 return [[commentsValue_ retain] autorelease]; 469} 470 471- (void)setCommentsValue:(NSString *)value { 472 if (commentsValue_ != value) { 473 [commentsValue_ release]; 474 commentsValue_ = [value copy]; 475 } 476} 477 478- (NSString *)emailValue { 479 return [[emailValue_ retain] autorelease]; 480} 481 482- (void)setEmailValue:(NSString *)value { 483 if (emailValue_ != value) { 484 [emailValue_ release]; 485 emailValue_ = [value copy]; 486 } 487} 488 489- (NSString *)countdownMessage { 490 return [[countdownMessage_ retain] autorelease]; 491} 492 493- (void)setCountdownMessage:(NSString *)value { 494 if (countdownMessage_ != value) { 495 [countdownMessage_ release]; 496 countdownMessage_ = [value copy]; 497 } 498} 499 500#pragma mark - 501//============================================================================= 502- (BOOL)reportIntervalElapsed { 503 float interval = [[[uploader_ parameters] 504 objectForKey:@BREAKPAD_REPORT_INTERVAL] floatValue]; 505 NSString *program = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; 506 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 507 NSMutableDictionary *programDict = 508 [NSMutableDictionary dictionaryWithDictionary:[ud dictionaryForKey:program]]; 509 NSNumber *lastTimeNum = [programDict objectForKey:kLastSubmission]; 510 NSTimeInterval lastTime = lastTimeNum ? [lastTimeNum floatValue] : 0; 511 NSTimeInterval now = CFAbsoluteTimeGetCurrent(); 512 NSTimeInterval spanSeconds = (now - lastTime); 513 514 [programDict setObject:[NSNumber numberWithDouble:now] 515 forKey:kLastSubmission]; 516 [ud setObject:programDict forKey:program]; 517 [ud synchronize]; 518 519 // If we've specified an interval and we're within that time, don't ask the 520 // user if we should report 521 GTMLoggerDebug(@"Reporter Interval: %f", interval); 522 if (interval > spanSeconds) { 523 GTMLoggerDebug(@"Within throttling interval, not sending report"); 524 return NO; 525 } 526 return YES; 527} 528 529- (BOOL)isOnDemand { 530 return [[[uploader_ parameters] objectForKey:@BREAKPAD_ON_DEMAND] 531 isEqualToString:@"YES"]; 532} 533 534- (BOOL)shouldSubmitSilently { 535 return [[[uploader_ parameters] objectForKey:@BREAKPAD_SKIP_CONFIRM] 536 isEqualToString:@"YES"]; 537} 538 539- (BOOL)shouldRequestComments { 540 return [[[uploader_ parameters] objectForKey:@BREAKPAD_REQUEST_COMMENTS] 541 isEqualToString:@"YES"]; 542} 543 544- (BOOL)shouldRequestEmail { 545 return [[[uploader_ parameters] objectForKey:@BREAKPAD_REQUEST_EMAIL] 546 isEqualToString:@"YES"]; 547} 548 549- (NSString*)shortDialogMessage { 550 NSString *displayName = 551 [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; 552 if (![displayName length]) 553 displayName = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; 554 555 if ([self isOnDemand]) { 556 return [NSString 557 stringWithFormat:NSLocalizedString(@"noCrashDialogHeader", @""), 558 displayName]; 559 } else { 560 return [NSString 561 stringWithFormat:NSLocalizedString(@"crashDialogHeader", @""), 562 displayName]; 563 } 564} 565 566- (NSString*)explanatoryDialogText { 567 NSString *displayName = 568 [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; 569 if (![displayName length]) 570 displayName = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; 571 572 NSString *vendor = [[uploader_ parameters] objectForKey:@BREAKPAD_VENDOR]; 573 if (![vendor length]) 574 vendor = @"unknown vendor"; 575 576 if ([self isOnDemand]) { 577 return [NSString 578 stringWithFormat:NSLocalizedString(@"noCrashDialogMsg", @""), 579 vendor, displayName]; 580 } else { 581 return [NSString 582 stringWithFormat:NSLocalizedString(@"crashDialogMsg", @""), 583 vendor]; 584 } 585} 586 587- (NSTimeInterval)messageTimeout { 588 // Get the timeout value for the notification. 589 NSTimeInterval timeout = [[[uploader_ parameters] 590 objectForKey:@BREAKPAD_CONFIRM_TIMEOUT] floatValue]; 591 // Require a timeout of at least a minute (except 0, which means no timeout). 592 if (timeout > 0.001 && timeout < 60.0) { 593 timeout = 60.0; 594 } 595 return timeout; 596} 597 598- (void)report { 599 [uploader_ report]; 600} 601 602//============================================================================= 603- (void)dealloc { 604 [uploader_ release]; 605 [super dealloc]; 606} 607 608- (void)awakeFromNib { 609 [emailEntryField_ setMaximumLength:kEmailMaxLength]; 610 [commentsEntryField_ setMaximumLength:kUserCommentsMaxLength]; 611} 612 613@end 614 615//============================================================================= 616@implementation LengthLimitingTextField 617 618- (void)setMaximumLength:(NSUInteger)maxLength { 619 maximumLength_ = maxLength; 620} 621 622// This is the method we're overriding in NSTextField, which lets us 623// limit the user's input if it makes the string too long. 624- (BOOL) textView:(NSTextView *)textView 625shouldChangeTextInRange:(NSRange)affectedCharRange 626 replacementString:(NSString *)replacementString { 627 628 // Sometimes the range comes in invalid, so reject if we can't 629 // figure out if the replacement text is too long. 630 if (affectedCharRange.location == NSNotFound) { 631 return NO; 632 } 633 // Figure out what the new string length would be, taking into 634 // account user selections. 635 NSUInteger newStringLength = 636 [[textView string] length] - affectedCharRange.length + 637 [replacementString length]; 638 if (newStringLength > maximumLength_) { 639 return NO; 640 } else { 641 return YES; 642 } 643} 644 645// Cut, copy, and paste have to be caught specifically since there is no menu. 646- (BOOL)performKeyEquivalent:(NSEvent*)event { 647 // Only handle the key equivalent if |self| is the text field with focus. 648 NSText* fieldEditor = [self currentEditor]; 649 if (fieldEditor != nil) { 650 // Check for a single "Command" modifier 651 NSUInteger modifiers = [event modifierFlags]; 652 modifiers &= NSDeviceIndependentModifierFlagsMask; 653 if (modifiers == NSCommandKeyMask) { 654 // Now, check for Select All, Cut, Copy, or Paste key equivalents. 655 NSString* characters = [event characters]; 656 // Select All is Command-A. 657 if ([characters isEqualToString:@"a"]) { 658 [fieldEditor selectAll:self]; 659 return YES; 660 // Cut is Command-X. 661 } else if ([characters isEqualToString:@"x"]) { 662 [fieldEditor cut:self]; 663 return YES; 664 // Copy is Command-C. 665 } else if ([characters isEqualToString:@"c"]) { 666 [fieldEditor copy:self]; 667 return YES; 668 // Paste is Command-V. 669 } else if ([characters isEqualToString:@"v"]) { 670 [fieldEditor paste:self]; 671 return YES; 672 } 673 } 674 } 675 // Let the super class handle the rest (e.g. Command-Period will cancel). 676 return [super performKeyEquivalent:event]; 677} 678 679@end 680 681//============================================================================= 682int main(int argc, const char *argv[]) { 683 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 684#if DEBUG 685 // Log to stderr in debug builds. 686 [GTMLogger setSharedLogger:[GTMLogger standardLoggerWithStderr]]; 687#endif 688 GTMLoggerDebug(@"Reporter Launched, argc=%d", argc); 689 // The expectation is that there will be one argument which is the path 690 // to the configuration file 691 if (argc != 2) { 692 exit(1); 693 } 694 695 Reporter *reporter = [[Reporter alloc] initWithConfigFile:argv[1]]; 696 if (!reporter) { 697 GTMLoggerDebug(@"reporter initialization failed"); 698 exit(1); 699 } 700 701 // only submit a report if we have not recently crashed in the past 702 BOOL shouldSubmitReport = [reporter reportIntervalElapsed]; 703 BOOL okayToSend = NO; 704 705 // ask user if we should send 706 if (shouldSubmitReport) { 707 if ([reporter shouldSubmitSilently]) { 708 GTMLoggerDebug(@"Skipping confirmation and sending report"); 709 okayToSend = YES; 710 } else { 711 okayToSend = [reporter askUserPermissionToSend]; 712 } 713 } 714 715 // If we're running as root, switch over to nobody 716 if (getuid() == 0 || geteuid() == 0) { 717 struct passwd *pw = getpwnam("nobody"); 718 719 // If we can't get a non-root uid, don't send the report 720 if (!pw) { 721 GTMLoggerDebug(@"!pw - %s", strerror(errno)); 722 exit(0); 723 } 724 725 if (setgid(pw->pw_gid) == -1) { 726 GTMLoggerDebug(@"setgid(pw->pw_gid) == -1 - %s", strerror(errno)); 727 exit(0); 728 } 729 730 if (setuid(pw->pw_uid) == -1) { 731 GTMLoggerDebug(@"setuid(pw->pw_uid) == -1 - %s", strerror(errno)); 732 exit(0); 733 } 734 } 735 else { 736 GTMLoggerDebug(@"getuid() !=0 || geteuid() != 0"); 737 } 738 739 if (okayToSend && shouldSubmitReport) { 740 GTMLoggerDebug(@"Sending Report"); 741 [reporter report]; 742 GTMLoggerDebug(@"Report Sent!"); 743 } else { 744 GTMLoggerDebug(@"Not sending crash report okayToSend=%d, "\ 745 "shouldSubmitReport=%d", okayToSend, shouldSubmitReport); 746 } 747 748 GTMLoggerDebug(@"Exiting with no errors"); 749 // Cleanup 750 [reporter release]; 751 [pool release]; 752 return 0; 753}