xref: /aosp_15_r20/external/google-breakpad/src/client/mac/sender/crash_report_sender.m (revision 9712c20fc9bbfbac4935993a2ca0b3958c5adad2)
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