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