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 = 269 NSGetAlertPanel([self shortDialogMessage], 270 @"%@", 271 NSLocalizedString(@"sendReportButton", @""), 272 NSLocalizedString(@"cancelButton", @""), 273 nil, 274 [self explanatoryDialogText]); 275 276 // Pop the alert with an automatic timeout, and wait for the response 277 buttonPressed = [self runModalWindow:alert withTimeout:timeout]; 278 279 // Release the panel memory 280 NSReleaseAlertPanel(alert); 281 } 282 return buttonPressed == NSAlertDefaultReturn; 283} 284 285- (void)configureAlertWindowIncludingEmail:(BOOL)includeEmail { 286 // Swap in localized values, making size adjustments to impacted elements as 287 // we go. Remember that the origin is in the bottom left, so elements above 288 // "fall" as text areas are shrunk from their overly-large IB sizes. 289 290 // Localize the header. No resizing needed, as it has plenty of room. 291 [dialogTitle_ setStringValue:[self shortDialogMessage]]; 292 293 // Localize the explanatory text field. 294 [commentMessage_ setStringValue:[NSString stringWithFormat:@"%@\n\n%@", 295 [self explanatoryDialogText], 296 NSLocalizedString(@"commentsMsg", @"")]]; 297 CGFloat commentHeightDelta = [commentMessage_ breakpad_adjustHeightToFit]; 298 [headerBox_ breakpad_shiftVertically:commentHeightDelta]; 299 [alertWindow_ breakpad_adjustHeight:commentHeightDelta]; 300 301 // Either localize the email explanation field or remove the whole email 302 // section depending on whether or not we are asking for email. 303 if (includeEmail) { 304 [emailMessage_ setStringValue:NSLocalizedString(@"emailMsg", @"")]; 305 CGFloat emailHeightDelta = [emailMessage_ breakpad_adjustHeightToFit]; 306 [preEmailBox_ breakpad_shiftVertically:emailHeightDelta]; 307 [alertWindow_ breakpad_adjustHeight:emailHeightDelta]; 308 } else { 309 [self removeEmailPrompt]; // Handles necessary resizing. 310 } 311 312 // Localize the email label, and shift the associated text field. 313 [emailLabel_ setStringValue:NSLocalizedString(@"emailLabel", @"")]; 314 CGFloat emailLabelWidthDelta = [emailLabel_ breakpad_adjustWidthToFit]; 315 [emailEntryField_ breakpad_shiftHorizontally:emailLabelWidthDelta]; 316 317 // Localize the privacy policy label, and keep it right-aligned to the arrow. 318 [privacyLinkLabel_ setStringValue:NSLocalizedString(@"privacyLabel", @"")]; 319 CGFloat privacyLabelWidthDelta = 320 [privacyLinkLabel_ breakpad_adjustWidthToFit]; 321 [privacyLinkLabel_ breakpad_shiftHorizontally:(-privacyLabelWidthDelta)]; 322 323 // Ensure that the email field and the privacy policy link don't overlap. 324 CGFloat kMinControlPadding = 8; 325 CGFloat maxEmailFieldWidth = NSMinX([privacyLinkLabel_ frame]) - 326 NSMinX([emailEntryField_ frame]) - 327 kMinControlPadding; 328 if (NSWidth([emailEntryField_ bounds]) > maxEmailFieldWidth && 329 maxEmailFieldWidth > 0) { 330 NSSize emailSize = [emailEntryField_ frame].size; 331 emailSize.width = maxEmailFieldWidth; 332 [emailEntryField_ setFrameSize:emailSize]; 333 } 334 335 // Localize the placeholder text. 336 [[commentsEntryField_ cell] 337 setPlaceholderString:NSLocalizedString(@"commentsPlaceholder", @"")]; 338 [[emailEntryField_ cell] 339 setPlaceholderString:NSLocalizedString(@"emailPlaceholder", @"")]; 340 341 // Localize the buttons, and keep the cancel button at the right distance. 342 [sendButton_ setTitle:NSLocalizedString(@"sendReportButton", @"")]; 343 CGFloat sendButtonWidthDelta = [sendButton_ breakpad_smartSizeToFit]; 344 [cancelButton_ breakpad_shiftHorizontally:(-sendButtonWidthDelta)]; 345 [cancelButton_ setTitle:NSLocalizedString(@"cancelButton", @"")]; 346 [cancelButton_ breakpad_smartSizeToFit]; 347} 348 349- (void)removeEmailPrompt { 350 [emailSectionBox_ setHidden:YES]; 351 CGFloat emailSectionHeight = NSHeight([emailSectionBox_ frame]); 352 [preEmailBox_ breakpad_shiftVertically:(-emailSectionHeight)]; 353 [alertWindow_ breakpad_adjustHeight:(-emailSectionHeight)]; 354} 355 356- (NSInteger)runModalWindow:(NSWindow*)window 357 withTimeout:(NSTimeInterval)timeout { 358 // Queue a |stopModal| message to be performed in |timeout| seconds. 359 if (timeout > 0.001) { 360 remainingDialogTime_ = timeout; 361 SEL updateSelector = @selector(updateSecondsLeftInDialogDisplay:); 362 messageTimer_ = [NSTimer scheduledTimerWithTimeInterval:1.0 363 target:self 364 selector:updateSelector 365 userInfo:nil 366 repeats:YES]; 367 } 368 369 // Run the window modally and wait for either a |stopModal| message or a 370 // button click. 371 [NSApp activateIgnoringOtherApps:YES]; 372 NSInteger returnMethod = [NSApp runModalForWindow:window]; 373 374 return returnMethod; 375} 376 377- (IBAction)sendReport:(id)sender { 378 // Force the text fields to end editing so text for the currently focused 379 // field will be commited. 380 [alertWindow_ makeFirstResponder:alertWindow_]; 381 382 [alertWindow_ orderOut:self]; 383 // Use NSAlertDefaultReturn so that the return value of |runModalWithWindow| 384 // matches the AppKit function NSRunAlertPanel() 385 [NSApp stopModalWithCode:NSAlertDefaultReturn]; 386} 387 388// UI Button Actions 389//============================================================================= 390- (IBAction)cancel:(id)sender { 391 [alertWindow_ orderOut:self]; 392 // Use NSAlertDefaultReturn so that the return value of |runModalWithWindow| 393 // matches the AppKit function NSRunAlertPanel() 394 [NSApp stopModalWithCode:NSAlertAlternateReturn]; 395} 396 397- (IBAction)showPrivacyPolicy:(id)sender { 398 // Get the localized privacy policy URL and open it in the default browser. 399 NSURL* privacyPolicyURL = 400 [NSURL URLWithString:NSLocalizedString(@"privacyPolicyURL", @"")]; 401 [[NSWorkspace sharedWorkspace] openURL:privacyPolicyURL]; 402} 403 404// Text Field Delegate Methods 405//============================================================================= 406- (BOOL) control:(NSControl*)control 407 textView:(NSTextView*)textView 408doCommandBySelector:(SEL)commandSelector { 409 BOOL result = NO; 410 // If the user has entered text on the comment field, don't end 411 // editing on "return". 412 if (control == commentsEntryField_ && 413 commandSelector == @selector(insertNewline:) 414 && [[textView string] length] > 0) { 415 [textView insertNewlineIgnoringFieldEditor:self]; 416 result = YES; 417 } 418 return result; 419} 420 421- (void)controlTextDidBeginEditing:(NSNotification *)aNotification { 422 [messageTimer_ invalidate]; 423 [self setCountdownMessage:@""]; 424} 425 426- (void)updateSecondsLeftInDialogDisplay:(NSTimer*)theTimer { 427 remainingDialogTime_ -= 1; 428 429 NSString *countdownMessage; 430 NSString *formatString; 431 432 int displayedTimeLeft; // This can be either minutes or seconds. 433 434 if (remainingDialogTime_ > 59) { 435 // calculate minutes remaining for UI purposes 436 displayedTimeLeft = (int)(remainingDialogTime_ / 60); 437 438 if (displayedTimeLeft == 1) { 439 formatString = NSLocalizedString(@"countdownMsgMinuteSingular", @""); 440 } else { 441 formatString = NSLocalizedString(@"countdownMsgMinutesPlural", @""); 442 } 443 } else { 444 displayedTimeLeft = (int)remainingDialogTime_; 445 if (displayedTimeLeft == 1) { 446 formatString = NSLocalizedString(@"countdownMsgSecondSingular", @""); 447 } else { 448 formatString = NSLocalizedString(@"countdownMsgSecondsPlural", @""); 449 } 450 } 451 countdownMessage = [NSString stringWithFormat:formatString, 452 displayedTimeLeft]; 453 if (remainingDialogTime_ <= 30) { 454 [countdownLabel_ setTextColor:[NSColor redColor]]; 455 } 456 [self setCountdownMessage:countdownMessage]; 457 if (remainingDialogTime_ <= 0) { 458 [messageTimer_ invalidate]; 459 [NSApp stopModal]; 460 } 461} 462 463 464 465#pragma mark Accessors 466#pragma mark - 467//============================================================================= 468 469- (NSString *)commentsValue { 470 return [[commentsValue_ retain] autorelease]; 471} 472 473- (void)setCommentsValue:(NSString *)value { 474 if (commentsValue_ != value) { 475 [commentsValue_ release]; 476 commentsValue_ = [value copy]; 477 } 478} 479 480- (NSString *)emailValue { 481 return [[emailValue_ retain] autorelease]; 482} 483 484- (void)setEmailValue:(NSString *)value { 485 if (emailValue_ != value) { 486 [emailValue_ release]; 487 emailValue_ = [value copy]; 488 } 489} 490 491- (NSString *)countdownMessage { 492 return [[countdownMessage_ retain] autorelease]; 493} 494 495- (void)setCountdownMessage:(NSString *)value { 496 if (countdownMessage_ != value) { 497 [countdownMessage_ release]; 498 countdownMessage_ = [value copy]; 499 } 500} 501 502#pragma mark - 503//============================================================================= 504- (BOOL)reportIntervalElapsed { 505 float interval = [[[uploader_ parameters] 506 objectForKey:@BREAKPAD_REPORT_INTERVAL] floatValue]; 507 NSString *program = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; 508 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 509 NSMutableDictionary *programDict = 510 [NSMutableDictionary dictionaryWithDictionary:[ud dictionaryForKey:program]]; 511 NSNumber *lastTimeNum = [programDict objectForKey:kLastSubmission]; 512 NSTimeInterval lastTime = lastTimeNum ? [lastTimeNum floatValue] : 0; 513 NSTimeInterval now = CFAbsoluteTimeGetCurrent(); 514 NSTimeInterval spanSeconds = (now - lastTime); 515 516 [programDict setObject:[NSNumber numberWithDouble:now] 517 forKey:kLastSubmission]; 518 [ud setObject:programDict forKey:program]; 519 [ud synchronize]; 520 521 // If we've specified an interval and we're within that time, don't ask the 522 // user if we should report 523 GTMLoggerDebug(@"Reporter Interval: %f", interval); 524 if (interval > spanSeconds) { 525 GTMLoggerDebug(@"Within throttling interval, not sending report"); 526 return NO; 527 } 528 return YES; 529} 530 531- (BOOL)isOnDemand { 532 return [[[uploader_ parameters] objectForKey:@BREAKPAD_ON_DEMAND] 533 isEqualToString:@"YES"]; 534} 535 536- (BOOL)shouldSubmitSilently { 537 return [[[uploader_ parameters] objectForKey:@BREAKPAD_SKIP_CONFIRM] 538 isEqualToString:@"YES"]; 539} 540 541- (BOOL)shouldRequestComments { 542 return [[[uploader_ parameters] objectForKey:@BREAKPAD_REQUEST_COMMENTS] 543 isEqualToString:@"YES"]; 544} 545 546- (BOOL)shouldRequestEmail { 547 return [[[uploader_ parameters] objectForKey:@BREAKPAD_REQUEST_EMAIL] 548 isEqualToString:@"YES"]; 549} 550 551- (NSString*)shortDialogMessage { 552 NSString *displayName = 553 [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; 554 if (![displayName length]) 555 displayName = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; 556 557 if ([self isOnDemand]) { 558 // Local variable to pacify clang's -Wformat-extra-args. 559 NSString* format = NSLocalizedString(@"noCrashDialogHeader", @""); 560 return [NSString stringWithFormat:format, displayName]; 561 } else { 562 // Local variable to pacify clang's -Wformat-extra-args. 563 NSString* format = NSLocalizedString(@"crashDialogHeader", @""); 564 return [NSString stringWithFormat:format, displayName]; 565 } 566} 567 568- (NSString*)explanatoryDialogText { 569 NSString *displayName = 570 [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT_DISPLAY]; 571 if (![displayName length]) 572 displayName = [[uploader_ parameters] objectForKey:@BREAKPAD_PRODUCT]; 573 574 NSString *vendor = [[uploader_ parameters] objectForKey:@BREAKPAD_VENDOR]; 575 if (![vendor length]) 576 vendor = @"unknown vendor"; 577 578 if ([self isOnDemand]) { 579 // Local variable to pacify clang's -Wformat-extra-args. 580 NSString* format = NSLocalizedString(@"noCrashDialogMsg", @""); 581 return [NSString stringWithFormat:format, vendor, displayName]; 582 } else { 583 // Local variable to pacify clang's -Wformat-extra-args. 584 NSString* format = NSLocalizedString(@"crashDialogMsg", @""); 585 return [NSString stringWithFormat:format, vendor]; 586 } 587} 588 589- (NSTimeInterval)messageTimeout { 590 // Get the timeout value for the notification. 591 NSTimeInterval timeout = [[[uploader_ parameters] 592 objectForKey:@BREAKPAD_CONFIRM_TIMEOUT] floatValue]; 593 // Require a timeout of at least a minute (except 0, which means no timeout). 594 if (timeout > 0.001 && timeout < 60.0) { 595 timeout = 60.0; 596 } 597 return timeout; 598} 599 600- (void)report { 601 [uploader_ report]; 602} 603 604//============================================================================= 605- (void)dealloc { 606 [uploader_ release]; 607 [super dealloc]; 608} 609 610- (void)awakeFromNib { 611 [emailEntryField_ setMaximumLength:kEmailMaxLength]; 612 [commentsEntryField_ setMaximumLength:kUserCommentsMaxLength]; 613} 614 615@end 616 617//============================================================================= 618@implementation LengthLimitingTextField 619 620- (void)setMaximumLength:(NSUInteger)maxLength { 621 maximumLength_ = maxLength; 622} 623 624// This is the method we're overriding in NSTextField, which lets us 625// limit the user's input if it makes the string too long. 626- (BOOL) textView:(NSTextView *)textView 627shouldChangeTextInRange:(NSRange)affectedCharRange 628 replacementString:(NSString *)replacementString { 629 630 // Sometimes the range comes in invalid, so reject if we can't 631 // figure out if the replacement text is too long. 632 if (affectedCharRange.location == NSNotFound) { 633 return NO; 634 } 635 // Figure out what the new string length would be, taking into 636 // account user selections. 637 NSUInteger newStringLength = 638 [[textView string] length] - affectedCharRange.length + 639 [replacementString length]; 640 if (newStringLength > maximumLength_) { 641 return NO; 642 } else { 643 return YES; 644 } 645} 646 647// Cut, copy, and paste have to be caught specifically since there is no menu. 648- (BOOL)performKeyEquivalent:(NSEvent*)event { 649 // Only handle the key equivalent if |self| is the text field with focus. 650 NSText* fieldEditor = [self currentEditor]; 651 if (fieldEditor != nil) { 652 // Check for a single "Command" modifier 653 NSUInteger modifiers = [event modifierFlags]; 654 modifiers &= NSDeviceIndependentModifierFlagsMask; 655 if (modifiers == NSCommandKeyMask) { 656 // Now, check for Select All, Cut, Copy, or Paste key equivalents. 657 NSString* characters = [event characters]; 658 // Select All is Command-A. 659 if ([characters isEqualToString:@"a"]) { 660 [fieldEditor selectAll:self]; 661 return YES; 662 // Cut is Command-X. 663 } else if ([characters isEqualToString:@"x"]) { 664 [fieldEditor cut:self]; 665 return YES; 666 // Copy is Command-C. 667 } else if ([characters isEqualToString:@"c"]) { 668 [fieldEditor copy:self]; 669 return YES; 670 // Paste is Command-V. 671 } else if ([characters isEqualToString:@"v"]) { 672 [fieldEditor paste:self]; 673 return YES; 674 } 675 } 676 } 677 // Let the super class handle the rest (e.g. Command-Period will cancel). 678 return [super performKeyEquivalent:event]; 679} 680 681@end 682 683//============================================================================= 684int main(int argc, const char *argv[]) { 685 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 686#if DEBUG 687 // Log to stderr in debug builds. 688 [GTMLogger setSharedLogger:[GTMLogger standardLoggerWithStderr]]; 689#endif 690 GTMLoggerDebug(@"Reporter Launched, argc=%d", argc); 691 // The expectation is that there will be one argument which is the path 692 // to the configuration file 693 if (argc != 2) { 694 exit(1); 695 } 696 697 Reporter *reporter = [[Reporter alloc] initWithConfigFile:argv[1]]; 698 if (!reporter) { 699 GTMLoggerDebug(@"reporter initialization failed"); 700 exit(1); 701 } 702 703 // only submit a report if we have not recently crashed in the past 704 BOOL shouldSubmitReport = [reporter reportIntervalElapsed]; 705 BOOL okayToSend = NO; 706 707 // ask user if we should send 708 if (shouldSubmitReport) { 709 if ([reporter shouldSubmitSilently]) { 710 GTMLoggerDebug(@"Skipping confirmation and sending report"); 711 okayToSend = YES; 712 } else { 713 okayToSend = [reporter askUserPermissionToSend]; 714 } 715 } 716 717 // If we're running as root, switch over to nobody 718 if (getuid() == 0 || geteuid() == 0) { 719 struct passwd *pw = getpwnam("nobody"); 720 721 // If we can't get a non-root uid, don't send the report 722 if (!pw) { 723 GTMLoggerDebug(@"!pw - %s", strerror(errno)); 724 exit(0); 725 } 726 727 if (setgid(pw->pw_gid) == -1) { 728 GTMLoggerDebug(@"setgid(pw->pw_gid) == -1 - %s", strerror(errno)); 729 exit(0); 730 } 731 732 if (setuid(pw->pw_uid) == -1) { 733 GTMLoggerDebug(@"setuid(pw->pw_uid) == -1 - %s", strerror(errno)); 734 exit(0); 735 } 736 } 737 else { 738 GTMLoggerDebug(@"getuid() !=0 || geteuid() != 0"); 739 } 740 741 if (okayToSend && shouldSubmitReport) { 742 GTMLoggerDebug(@"Sending Report"); 743 [reporter report]; 744 GTMLoggerDebug(@"Report Sent!"); 745 } else { 746 GTMLoggerDebug(@"Not sending crash report okayToSend=%d, "\ 747 "shouldSubmitReport=%d", okayToSend, shouldSubmitReport); 748 } 749 750 GTMLoggerDebug(@"Exiting with no errors"); 751 // Cleanup 752 [reporter release]; 753 [pool release]; 754 return 0; 755} 756