13 | internal var md5: String {
14 | let length = Int(CC_MD5_DIGEST_LENGTH)
15 | let messageData = self.data(using: .utf8)!
16 | var digestData = Data(count: length)
17 |
18 | digestData.withUnsafeMutableBytes { digestBytes -> Void in
19 | messageData.withUnsafeBytes { messageBytes -> Void in
20 | guard let messageBytesBaseAddress = messageBytes.baseAddress else { return }
21 | guard let digestBytesBlindMemory = digestBytes.bindMemory(to: UInt8.self).baseAddress else { return }
22 | let messageLength = CC_LONG(messageData.count)
23 | CC_MD5(messageBytesBaseAddress, messageLength, digestBytesBlindMemory)
24 | }
25 | }
26 |
27 | return digestData.map { String(format: "%02hhx", $0) }.joined()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/CrashReporter/Infrastructure/URLResponse+statusIsOK.swift:
--------------------------------------------------------------------------------
1 | // Part of
2 | //
3 | // Created by Brent Simmons on 8/14/16.
4 | // Copyright © 2016 Ranchero Software, LLC. All rights reserved.
5 | // Copyright © 2019 Christian Tietze. All rights reserved.
6 | // Distributed under the MIT License.
7 |
8 | import class Foundation.URLResponse
9 | import class Foundation.HTTPURLResponse
10 |
11 | extension URLResponse {
12 | internal var statusIsOK: Bool {
13 | guard let response = self as? HTTPURLResponse else { return false }
14 | return (200...299).contains(response.statusCode)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/CrashReporter/SendReportsAutomaticallySetting.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2019 Christian Tietze. All rights reserved.
2 | // Distributed under the MIT License.
3 |
4 | import Foundation
5 |
6 | struct SendReportsAutomaticallySetting {
7 | static var standard: SendReportsAutomaticallySetting {
8 | return SendReportsAutomaticallySetting(
9 | isVisible: true,
10 | userDefaults: .standard,
11 | sendCrashLogsAutomaticallyKey: CrashReporter.DefaultsKeys.standard.sendCrashLogsAutomaticallyKey)
12 | }
13 |
14 | let isVisible: Bool
15 |
16 | let userDefaults: UserDefaults
17 | let sendCrashLogsAutomaticallyKey: String
18 |
19 | var isEnabled: Bool {
20 | get {
21 | return userDefaults.bool(forKey: sendCrashLogsAutomaticallyKey)
22 | }
23 | set {
24 | userDefaults.set(newValue, forKey: sendCrashLogsAutomaticallyKey)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/CrashReporter/UI/CrashReportWindowController.swift:
--------------------------------------------------------------------------------
1 | // Part of
2 | //
3 | // Created by Brent Simmons on 12/28/18.
4 | // Copyright © 2018 Ranchero Software. All rights reserved.
5 | // Copyright © 2019 Christian Tietze. All rights reserved.
6 | // Distributed under the MIT License.
7 |
8 | import AppKit
9 |
10 | protocol SendsCrashLog {
11 | func send(emailAddress: String?, userProvidedDetails: String?, crashLogText: String)
12 | }
13 |
14 | final class CrashReportWindowController: NSWindowController, NSWindowDelegate {
15 |
16 | convenience init(
17 | appName: String,
18 | crashLogText: String,
19 | crashLogSender: SendsCrashLog,
20 | privacyPolicyURL: URL,
21 | collectEmailSetting: EmailAddressSetting,
22 | sendReportsAutomaticallySetting: SendReportsAutomaticallySetting
23 | ) {
24 | self.init()
25 |
26 | var nibTopLevelObjects: NSArray?
27 | CrashReporterBundle.loadNibNamed(
28 | "CrashReporterWindow", owner: self, topLevelObjects: &nibTopLevelObjects)
29 | self.window = nibTopLevelObjects?.lazy.compactMap({ $0 as? NSWindow }).first
30 |
31 | self.crashLogText = crashLogText
32 | self.crashLogSender = crashLogSender
33 | self.privacyPolicyURL = privacyPolicyURL
34 | self.collectEmailSetting = collectEmailSetting
35 | self.sendReportsAutomaticallySetting = sendReportsAutomaticallySetting
36 |
37 | // Setup window
38 | window?.center()
39 | window?.delegate = self
40 |
41 | window?.title = "\(appName) Crash Reporter"
42 | titleLabel.stringValue = "\(appName) quit unexpectedly."
43 | crashLogContainerView.isHidden = true
44 |
45 | updateCrashLogText()
46 | updateCollectEmailVisibility()
47 | updateAutomaticallySendCrashLogVisibility()
48 | updateButtonStates()
49 | }
50 |
51 | var onWindowWillClose: ((NSWindow?) -> Void)?
52 |
53 | func windowWillClose(_ notification: Notification) {
54 | onWindowWillClose?(notification.object as? NSWindow)
55 | }
56 |
57 | // MARK: View components
58 |
59 | @IBOutlet var textView: NSTextView! {
60 | didSet {
61 | textView.font = NSFont.userFixedPitchFont(ofSize: 0.0)
62 | textView.textContainerInset = NSSize(width: 5.0, height: 5.0)
63 | updateCrashLogText()
64 | }
65 | }
66 |
67 | @IBOutlet var titleLabel: NSTextField!
68 | @IBOutlet var bodyLabel: NSTextField!
69 |
70 | @IBOutlet weak var collectEmailContainerView: NSView!
71 | @IBOutlet weak var crashLogContainerView: NSView!
72 | @IBOutlet weak var sendAutomaticallyContainerView: NSView!
73 |
74 | @IBOutlet var sendCrashLogButton: NSButton!
75 | @IBOutlet var dontSendButton: NSButton!
76 | @IBOutlet var toggleCrashLogButton: NSButton!
77 |
78 | private func updateCrashLogText() {
79 | guard let textView = self.textView else { return }
80 | textView.string = crashLogText ?? ""
81 | }
82 |
83 | private func updateButtonStates() {
84 | sendCrashLogButton?.isEnabled = (crashLogSender != nil) && !didSendCrashLog
85 | dontSendButton?.isEnabled = !didSendCrashLog
86 | }
87 |
88 | private func updateCollectEmailVisibility() {
89 | collectEmailContainerView.isHidden = self.hideCollectEmail
90 | bodyLabel.stringValue =
91 | "Help us fix crashes by submitting this crash report."
92 | + (self.hideCollectEmail
93 | ? ""
94 | : " You can include your email address if you agree to being contacted for more details.")
95 | }
96 |
97 | private func updateAutomaticallySendCrashLogVisibility() {
98 | sendAutomaticallyContainerView.isHidden = self.hideAutomaticallySend
99 | }
100 |
101 | // MARK: Model
102 |
103 | internal var collectEmailSetting: EmailAddressSetting = .standard {
104 | didSet {
105 | updateCollectEmailVisibility()
106 | }
107 | }
108 |
109 | internal var hideCollectEmail: Bool {
110 | return !collectEmailSetting.isVisible
111 | }
112 |
113 | internal var sendReportsAutomaticallySetting: SendReportsAutomaticallySetting = .standard {
114 | didSet {
115 | updateAutomaticallySendCrashLogVisibility()
116 | }
117 | }
118 |
119 | internal var hideAutomaticallySend: Bool {
120 | return !sendReportsAutomaticallySetting.isVisible
121 | }
122 |
123 | /// KVC wrapper for `sendReportsAutomaticallySetting.isEnabled`
124 | @objc dynamic var sendCrashReportsAutomatically: Bool {
125 | get {
126 | return sendReportsAutomaticallySetting.isEnabled
127 | }
128 | set {
129 | sendReportsAutomaticallySetting.isEnabled = newValue
130 | }
131 | }
132 |
133 | /// KVC wrapper for `collectEmailSetting.emailAddress`
134 | @objc dynamic var emailAddress: String {
135 | get {
136 | return collectEmailSetting.emailAddress ?? ""
137 | }
138 | set {
139 | collectEmailSetting.emailAddress = newValue
140 | }
141 | }
142 |
143 | @objc dynamic var userProvidedDetails = ""
144 |
145 | internal var privacyPolicyURL: URL?
146 |
147 | internal var crashLogText: String? {
148 | didSet {
149 | updateCrashLogText()
150 | }
151 | }
152 |
153 | internal var crashLogSender: SendsCrashLog? {
154 | didSet {
155 | updateButtonStates()
156 | }
157 | }
158 |
159 | private var didSendCrashLog = false {
160 | didSet {
161 | updateButtonStates()
162 | }
163 | }
164 |
165 | // MARK: - User Interactions
166 |
167 | lazy var isRunningTests: Bool = false
168 |
169 | @IBAction func sendCrashReport(_ sender: Any?) {
170 | guard !didSendCrashLog else { return }
171 | defer { didSendCrashLog = true }
172 |
173 | if !isRunningTests,
174 | let crashLogText = self.crashLogText,
175 | let crashLogSender = self.crashLogSender
176 | {
177 |
178 | let emailAddress = self.collectEmailSetting.isVisible ? self.emailAddress : nil
179 | crashLogSender.send(emailAddress: emailAddress, userProvidedDetails: userProvidedDetails, crashLogText: crashLogText)
180 | }
181 |
182 | close()
183 | }
184 |
185 | @IBAction func dontSendCrashReport(_ sender: Any?) {
186 | close()
187 | }
188 |
189 | override func responds(to aSelector: Selector!) -> Bool {
190 | if aSelector == #selector(showPrivacyPolicy(_:)) {
191 | return self.privacyPolicyURL != nil
192 | }
193 | return super.responds(to: aSelector)
194 | }
195 |
196 | @IBAction func showPrivacyPolicy(_ sender: Any?) {
197 | guard let privacyPolicyURL = self.privacyPolicyURL else { return }
198 | NSWorkspace.shared.open(privacyPolicyURL)
199 | }
200 |
201 | @IBAction func toggleCrashLog(_ sender: Any?) {
202 | crashLogContainerView.isHidden = !crashLogContainerView.isHidden
203 | toggleCrashLogButton.title =
204 | crashLogContainerView.isHidden ? "Show Details" : "Hide Details"
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/Sources/CrashReporter/UI/CrashReporterWindow.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | Help us fix crashes by submitting this crash report. You can include your email address if you agree to being contacted for more details.
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Your email address (optional)
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | Describe how the crash occurred (optional)
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
233 |
234 |
235 |
248 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
--------------------------------------------------------------------------------
/Tests/CrashReporterTests/EmailAddressSettingTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2020 Christian Tietze. All rights reserved.
2 | // Distributed under the MIT License.
3 |
4 | import XCTest
5 | @testable import CrashReporter
6 |
7 | class EmailAddressSettingTests: XCTestCase {
8 |
9 | func testStandardReusesDefaultsKey() {
10 | XCTAssertEqual(EmailAddressSetting.standard.emailAddressKey,
11 | CrashReporter.DefaultsKeys.standard.emailAddressKey)
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/CrashReporterTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tests/CrashReporterTests/SendReportsAutomaticallySettingTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright © 2019 Christian Tietze. All rights reserved.
2 | // Distributed under the MIT License.
3 |
4 | import XCTest
5 | @testable import CrashReporter
6 |
7 | class SendReportsAutomaticallySettingTests: XCTestCase {
8 |
9 | func testStandardReusesDefaultsKey() {
10 | XCTAssertEqual(SendReportsAutomaticallySetting.standard.sendCrashLogsAutomaticallyKey,
11 | CrashReporter.DefaultsKeys.standard.sendCrashLogsAutomaticallyKey)
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/assets/reporter-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CleanCocoa/CrashReporter/30e31eac5cad1ec7f34330a3bd3feba14c357aec/assets/reporter-dark.png
--------------------------------------------------------------------------------
/assets/reporter-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CleanCocoa/CrashReporter/30e31eac5cad1ec7f34330a3bd3feba14c357aec/assets/reporter-light.png
--------------------------------------------------------------------------------
/php/index.php:
--------------------------------------------------------------------------------
1 |
33 | *
34 | *****************************************************************************/
35 |
36 |
37 | use PHPMailer\PHPMailer\PHPMailer;
38 | use PHPMailer\PHPMailer\Exception;
39 |
40 | require 'vendor/PHPMailer/src/Exception.php';
41 | require 'vendor/PHPMailer/src/PHPMailer.php';
42 | require 'vendor/PHPMailer/src/SMTP.php';
43 |
44 | function postString($key) {
45 | return array_key_exists($key, $_POST) ? $_POST[$key] : '';
46 | }
47 |
48 | function clean($userData) {
49 | $userData = htmlspecialchars($userData, ENT_IGNORE, 'utf-8');
50 | $userData = strip_tags($userData);
51 | $userData = trim($userData);
52 | return $userData;
53 | }
54 |
55 | function isEmpty($var) {
56 | return !isset($var) || strlen(trim($var)) == 0;
57 | }
58 |
59 | function sendEmailForReportAsFilenameForSender($path, $filename, $app, $userProvidedDetails = '', $userEmail = '') {
60 | $mail = new PHPMailer(true);
61 |
62 | //Server settings
63 | if (DEBUG) {
64 | $mail->Debugoutput = 'echo';
65 | $mail->SMTPDebug = 4;
66 | }
67 | $mail->isSMTP();
68 | $mail->Host = SMTP_HOST;
69 | $mail->SMTPAuth = True;
70 | $mail->Username = SMTP_USER;
71 | $mail->Password = SMTP_PASS;
72 | $mail->SMTPSecure = SMTP_SECURE;
73 | $mail->Port = SMTP_PORT;
74 |
75 | //Recipients
76 | $mail->setFrom(SENDER_EMAIL, SENDER_NAME);
77 | $mail->addAddress(SUPPORT_EMAIL, SUPPORT_NAME);
78 | if (!isEmpty($userEmail) && SEND_CC_TO_USER) {
79 | $mail->addCC($userEmail);
80 | }
81 |
82 | // Attachments
83 | $mail->addAttachment($path, $filename);
84 |
85 | // Content
86 | $mail->Subject = $app . ' crash log' . (!isEmpty($userEmail) ? ' from ' . clean($userEmail) : '');
87 |
88 | $message = 'Processed on: ' . date("Y-m-d H:i:s") . "
\r\n"
89 | . 'App: ' . $app . "
\r\n"
90 | . 'Sender: ' . (!isEmpty($userEmail) ? clean($userEmail) : 'unknown') . "
\r\n
\r\n"
91 | . (!isEmpty($userProvidedDetails) ? "User-provided details:\r\n" . $userProvidedDetails . "
\r\n" : '');
92 | $mail->Body = $message;
93 | $mail->AltBody = $message;
94 |
95 | $mail->send();
96 | }
97 |
98 | // Collect request data
99 | $crashlog = postString('crashlog');
100 | $userProvidedDetails = clean(postString('userProvidedDetails'));
101 | $app = clean($_SERVER['HTTP_USER_AGENT']);
102 |
103 | /*
104 | // To test sending email from this script via `php index.php`, provide replacement data
105 | $crashlog = "test crash log content";
106 | $app = "test app 2000";
107 | */
108 |
109 | if (isEmpty($crashlog) || isEmpty($app)) {
110 | header('X-PHP-Response-Code: 401', true, 401);
111 | die();
112 | }
113 |
114 | $logIsJSON = $crashlog[0] == '{';
115 | $filename = date("Y-m-d H.i.s") . ' ' . $app . ($logIsJSON ? '.ips' : '.crash');
116 | $userEmail = postString('userEmail');
117 |
118 | $tmpfile = tmpfile();
119 | try {
120 | // Write report to file
121 | fwrite($tmpfile, $crashlog);
122 | fseek($tmpfile, 0);
123 | $path = stream_get_meta_data($tmpfile)['uri'];
124 |
125 | sendEmailForReportAsFilenameForSender($path, $filename, $app, $userProvidedDetails, $userEmail);
126 | } catch (Exception $e) { // PHPMailer exception
127 | header('X-PHP-Response-Code: 400', true, 400);
128 | echo($e->getMessage());
129 | } catch (\Exception $e) { // Global PHP exception
130 | header('X-PHP-Response-Code: 400', true, 400);
131 | echo $e->getMessage();
132 | } finally {
133 | fclose($tmpfile);
134 | }
135 |
136 |
--------------------------------------------------------------------------------