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