PageRenderTime 127ms CodeModel.GetById 13ms app.highlight 107ms RepoModel.GetById 1ms app.codeStats 0ms

/thirdparty/breakpad/client/mac/sender/crash_report_sender.m

http://github.com/tomahawk-player/tomahawk
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}