PluginProbe
BugSnag Error Monitoring plugin / trunk
BugSnag Error Monitoring plugin vtrunk
trunk 1.0.0 1.0.1 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 2.0.0
bugsnag / bugsnag.php

bugsnag.php in BugSnag Error Monitoring plugin trunk, at bugsnag.php

404 lines 14.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: BugSnag Error Monitoring plugin
4 Plugin URI: https://bugsnag.com
5 Description: Automatically detects errors & crashes on your WordPress site using BugSnag to notify you by email, chat or issues system.
6 Version: 1.6.5
7 Author: BugSnag
8 Author URI: https://bugsnag.com
9 License: GPLv2 or later
10 */
11
12 use Bugsnag\Client;
13 use Bugsnag\Handler;
14 use Bugsnag\ErrorTypes;
15 use Bugsnag\Report;
16
17 class Bugsnag_Wordpress
18 {
19 private static $COMPOSER_AUTOLOADER = 'vendor/autoload.php';
20 private static $DEFAULT_NOTIFY_SEVERITIES = 'fatal,error';
21
22 private static $DISABLED_NOTIFIER_METHODS = array(
23 'setAutoCaptureSessions',
24 'shouldCaptureSessions'
25 );
26
27 private static $NOTIFIER = array(
28 'name' => 'Bugsnag Wordpress (Official)',
29 'version' => '2.0.0',
30 'url' => 'https://github.com/bugsnag/bugsnag-wordpress',
31 );
32
33 private Client $client;
34 private $apiKey;
35 private $notifySeverities;
36 private $redactedKeys;
37 private $appVersion;
38 private $notifyEndpoint;
39 private $releaseStageConfig;
40 private $pluginBase;
41
42 public function __construct()
43 {
44 // Activate bugsnag error monitoring as soon as possible
45 $this->activateBugsnag();
46
47 $this->pluginBase = 'bugsnag/bugsnag.php';
48
49 // Run init actions (loading wp user)
50 add_action('init', array($this, 'registerUser'));
51
52 // Load admin actions (admin links and pages)
53 add_action('admin_menu', array($this, 'adminMenuActions'));
54
55 // Load network admin menu if using multisite
56 add_action('network_admin_menu', array($this, 'networkAdminMenuActions'));
57
58 add_action('wp_ajax_test_bugsnag', array($this, 'testBugsnag'));
59 }
60
61 private function activateBugsnag()
62 {
63 $is_load_success = $this->requireBugsnagPhp();
64 if (!$is_load_success) {
65 error_log("Bugsnag Error: Couldn't activate Bugsnag Error Monitoring due to missing Bugsnag library!");
66
67 return;
68 }
69
70 // Load bugsnag settings
71 if (!get_site_option('bugsnag_network')) {
72 // Regular
73 $this->apiKey = get_option('bugsnag_api_key');
74 $this->notifySeverities = get_option('bugsnag_notify_severities');
75 $redactedKeysValue = get_option('bugsnag_redacted_keys');
76 $this->redactedKeys = $redactedKeysValue ? $redactedKeysValue : get_option('bugsnag_filterfields'); // Backwards compatibility
77 $this->appVersion = get_option('bugsnag_app_version');
78 $this->notifyEndpoint = get_option('bugsnag_notify_endpoint');
79 $this->releaseStageConfig = get_option('bugsnag_release_stage');
80 } else {
81 // Multisite
82 $this->apiKey = get_site_option('bugsnag_api_key');
83 $this->notifySeverities = get_site_option('bugsnag_notify_severities');
84 $redactedKeysValue = get_site_option('bugsnag_redacted_keys');
85 $this->redactedKeys = $redactedKeysValue ? $redactedKeysValue : get_site_option('bugsnag_filterfields'); // Backwards compatibility
86 $this->appVersion = get_site_option('bugsnag_app_version');
87 $this->notifyEndpoint = get_site_option('bugsnag_notify_endpoint');
88 $this->releaseStageConfig = get_site_option('bugsnag_release_stage');
89 }
90
91 $this->constructBugsnag();
92 }
93
94 private function constructBugsnag()
95 {
96 // Activate the bugsnag client
97 if (!empty($this->apiKey)) {
98 $this->client = Client::make($this->apiKey);
99
100 $this->client->setReleaseStage($this->releaseStage())
101 ->setErrorReportingLevel($this->errorReportingLevel())
102 ->setRedactedKeys($this->redactedKeys());
103
104 // Set app version if configured
105 if (!empty($this->appVersion)) {
106 $this->client->setAppVersion($this->appVersion);
107 }
108
109 // Set notify endpoint if configured
110 if (!empty($this->notifyEndpoint)) {
111 $this->client->setNotifyEndpoint($this->notifyEndpoint);
112 }
113
114 $this->client->getConfig()->mergeDeviceData(['runtimeVersions' => ['wordpress' => get_bloginfo('version')]]);
115
116 $this->client->setNotifier(self::$NOTIFIER);
117
118 // If handlers are not set, errors are still going to be reported
119 // to bugsnag, difference is execution will not stop.
120 //
121 // Can be useful to see inline errors and traces with xdebug too.
122 $set_error_and_exception_handlers = apply_filters(
123 'bugsnag_set_error_and_exception_handlers',
124 defined('BUGSNAG_SET_EXCEPTION_HANDLERS') ? BUGSNAG_SET_EXCEPTION_HANDLERS : true
125 );
126
127 if ($set_error_and_exception_handlers === true) {
128 // Hook up automatic error handling
129 Handler::register($this->client);
130 }
131 }
132 }
133
134 private function requireBugsnagPhp()
135 {
136 // Bugsnag-php was already loaded by some 3rd-party code, don't need to load it again.
137 if (class_exists('Bugsnag\Client')) {
138 return true;
139 }
140
141 // Try loading bugsnag-php with composer autoloader.
142 try {
143 require_once $this->relativePath(self::$COMPOSER_AUTOLOADER);
144 return true;
145 } catch (Exception $e) {
146 return false;
147 }
148 }
149
150 private function relativePath($path)
151 {
152 return dirname(__FILE__) . '/' . $path;
153 }
154
155 private function errorReportingLevel()
156 {
157 $notifySeverities = empty($this->notifySeverities) ? self::$DEFAULT_NOTIFY_SEVERITIES : $this->notifySeverities;
158 $level = 0;
159
160 $severities = explode(',', $notifySeverities);
161 foreach ($severities as $severity) {
162 $level |= ErrorTypes::getLevelsForSeverity($severity);
163 }
164
165 return $level;
166 }
167
168 private function redactedKeys()
169 {
170 $redacted_keys = apply_filters('bugsnag_redacted_keys', $this->redactedKeys);
171
172 // Array with empty string will break things.
173 if ($redacted_keys === '') {
174 return array();
175 }
176
177 return array_map('trim', explode("\n", $redacted_keys));
178 }
179
180 /**
181 * Set Release Stage.
182 *
183 * @return $release_stage_filtered Release Stage Filtered.
184 */
185 private function releaseStage()
186 {
187 // Use configured release stage if available
188 if (!empty($this->releaseStageConfig)) {
189 $release_stage = $this->releaseStageConfig;
190 } elseif (function_exists('wp_get_environment_type')) {
191 $release_stage = wp_get_environment_type(); // Defaults to production when not set.
192 } else {
193 $release_stage = defined('WP_ENV') ? WP_ENV : 'production';
194 }
195 $release_stage_filtered = apply_filters('bugsnag_release_stage', $release_stage);
196
197 return $release_stage_filtered;
198 }
199
200 // Action hooks
201 public function registerUser()
202 {
203 if (!$this->isStarted()) { // This might attempt to run before the client is configured.
204 return;
205 }
206 $this->client->registerCallback(function (Report $report) {
207 // Set the bugsnag user using the current WordPress user if available,
208 // set as anonymous otherwise.
209 $user = [];
210
211 if (is_user_logged_in()) {
212 $wp_user = wp_get_current_user();
213 $user['id'] = $wp_user->user_login;
214 $user['email'] = $wp_user->user_email;
215 $user['name'] = $wp_user->display_name;
216 } else {
217 $use_unsafe_spoofable_ip_address_getter = apply_filters('bugsnag_use_unsafe_spoofable_ip_address_getter', true);
218 $user['id'] = $use_unsafe_spoofable_ip_address_getter ?
219 $this->getClientIpAddressUnsafe() :
220 $this->getClientIpAddress();
221 $user['name'] = 'anonymous';
222 }
223
224 $report->setUser($user);
225 });
226 }
227
228 // Unsafe: client can spoof address.
229 // http://stackoverflow.com/questions/1634782/what-is-the-most-accurate-way-to-retrieve-a-users-correct-ip-address-in-php
230 private function getClientIpAddressUnsafe()
231 {
232 foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key) {
233 if (array_key_exists($key, $_SERVER) === true) {
234 foreach (explode(',', $_SERVER[$key]) as $ip) {
235 $ip = trim($ip);
236 if (filter_var($ip, FILTER_VALIDATE_IP) !== false) {
237 return $ip;
238 }
239 }
240 }
241 }
242 }
243
244 // Can not be spoofed, but can show ip of NAT or proxies.
245 private function getClientIpAddress()
246 {
247 return $_SERVER['REMOTE_ADDR'];
248 }
249
250 public function adminMenuActions()
251 {
252 if (!function_exists('is_plugin_active_for_network') || !is_plugin_active_for_network($this->pluginBase)) {
253 // Add the "settings" link to the Bugsnag row of plugins.php
254 add_filter('plugin_action_links', array($this, 'pluginActionLinksFilter'), 10, 2);
255
256 // Create the settings page
257 add_options_page('Bugsnag Settings', 'Bugsnag', 'manage_options', 'bugsnag', array($this, 'renderSettings'));
258 }
259 }
260
261 public function networkAdminMenuActions()
262 {
263 if (function_exists('is_plugin_active_for_network') && is_plugin_active_for_network($this->pluginBase)) {
264 // Create the network settings page
265 add_submenu_page('settings.php', 'Bugsnag Settings', 'Bugsnag', 'manage_network_options', 'bugsnag', array($this, 'renderSettings'));
266 }
267 }
268
269 private function updateNetworkSettings()
270 {
271 // Update options
272 update_site_option('bugsnag_api_key', isset($_POST['bugsnag_api_key']) ? $_POST['bugsnag_api_key'] : '');
273 update_site_option('bugsnag_notify_severities', isset($_POST['bugsnag_notify_severities']) ? $_POST['bugsnag_notify_severities'] : '');
274 update_site_option('bugsnag_redacted_keys', isset($_POST['bugsnag_redacted_keys']) ? $_POST['bugsnag_redacted_keys'] : '');
275 update_site_option('bugsnag_app_version', isset($_POST['bugsnag_app_version']) ? $_POST['bugsnag_app_version'] : '');
276 update_site_option('bugsnag_notify_endpoint', isset($_POST['bugsnag_notify_endpoint']) ? $_POST['bugsnag_notify_endpoint'] : '');
277 update_site_option('bugsnag_release_stage', isset($_POST['bugsnag_release_stage']) ? $_POST['bugsnag_release_stage'] : '');
278 update_site_option('bugsnag_network', true);
279
280 // Update variables
281 $this->apiKey = get_site_option('bugsnag_api_key');
282 $this->notifySeverities = get_site_option('bugsnag_notify_severities');
283 $this->redactedKeys = get_site_option('bugsnag_redacted_keys');
284 $this->appVersion = get_site_option('bugsnag_app_version');
285 $this->notifyEndpoint = get_site_option('bugsnag_notify_endpoint');
286 $this->releaseStageConfig = get_site_option('bugsnag_release_stage');
287
288 echo '<div class="updated"><p>Settings saved.</p></div>';
289 }
290
291 // Filter hooks
292 public function pluginActionLinksFilter($links, $file)
293 {
294 // Add the "settings" link to the Bugsnag plugin row
295 if (basename($file) == basename(__FILE__)) {
296 $settings_link = '<a href="options-general.php?page=bugsnag">Settings</a>';
297 array_push($links, $settings_link);
298 }
299
300 return $links;
301 }
302
303 public function testBugsnag()
304 {
305 // Verify nonce for CSRF protection
306 if (!wp_verify_nonce($_POST['_wpnonce'], 'test_bugsnag_nonce')) {
307 wp_die('Security check failed.');
308 }
309
310 $this->apiKey = $_POST['bugsnag_api_key'];
311 $this->notifySeverities = $_POST['bugsnag_notify_severities'];
312 $this->redactedKeys = $_POST['bugsnag_redacted_keys'];
313 $this->appVersion = $_POST['bugsnag_app_version'];
314 $this->notifyEndpoint = $_POST['bugsnag_notify_endpoint'];
315 $this->releaseStageConfig = $_POST['bugsnag_release_stage'];
316
317 $this->constructBugsnag();
318 $this->client->notifyError(
319 'BugsnagTest',
320 'Testing bugsnag',
321 function (Report $report) {
322 $report->setSeverity('info');
323 $report->setMetaData([
324 'notifier' => self::$NOTIFIER,
325 'docs' => array('url' => 'https://docs.bugsnag.com/platforms/php/wordpress/'),
326 ]);
327 }
328 );
329
330 die();
331 }
332
333 // Renderers
334 public function renderSettings()
335 {
336 if (!empty($_POST['action']) && $_POST['action'] == 'update') {
337 // Verify nonce for CSRF protection
338 if (!wp_verify_nonce($_POST['_wpnonce'], 'update-options')) {
339 wp_die('Security check failed. Please try again.');
340 }
341 $this->updateNetworkSettings($_POST);
342 }
343
344 include $this->relativePath('views/settings.php');
345 }
346
347 public function isStarted()
348 {
349 return isset($this->apiKey) && isset($this->client);
350 }
351
352 private function renderOption($name, $value, $current)
353 {
354 $selected = ($value == $current) ? ' selected="selected"' : '';
355 echo "<option value=\"$value\"$selected>$name</option>";
356 }
357
358 /**
359 * Fluent interface to $this->client, simply call the methods on this object and this will proxy them through.
360 *
361 * @param string $method
362 * @param array $arguments
363 *
364 * @return mixed
365 */
366 public function __call($method, $arguments)
367 {
368 // If we don't have an API key here then the plugin has not been setup, but
369 // methods are already being called. We can't forward these calls through
370 // because the client needs an API key on construction and we need to fail
371 // loudly so the user knows their site isn't setup correctly.
372 if (empty($this->apiKey)) {
373 throw new BadMethodCallException(
374 'No Bugsnag API Key set. Please enter your API Key on the Bugsnag Settings page.'
375 );
376 }
377
378 if (in_array($method, self::$DISABLED_NOTIFIER_METHODS)) {
379 throw new BadMethodCallException(sprintf('Method %s is disabled in BugSnag for Wordpress', $method));
380 }
381
382 if (method_exists($this->client, $method)) {
383 return call_user_func_array(array($this->client, $method), $arguments);
384 }
385
386 throw new BadMethodCallException(sprintf('Method %s does not exist on %s or Bugsnag\Client', $method, __CLASS__));
387 }
388 }
389
390 /**
391 * Add ability to define Bugsnag API Key as constant in wp-config.php.
392 *
393 * @return either the API from wp-config or false (to use the option value)
394 */
395 function bugsnag_define_api_key()
396 {
397 return defined('BUGSNAG_API_KEY') ? BUGSNAG_API_KEY : false;
398 }
399 add_filter('pre_option_bugsnag_api_key', 'bugsnag_define_api_key');
400 add_filter('pre_site_option_bugsnag_api_key', 'bugsnag_define_api_key');
401
402 global $bugsnagWordpress;
403 $bugsnagWordpress = new Bugsnag_Wordpress();
404