Home | History | Annotate | Download | only in sender
      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"
     44 const int kUserCommentsMaxLength = 1500;
     45 const 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
    408 doCommandBySelector:(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
    627 shouldChangeTextInRange:(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 //=============================================================================
    684 int 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