PluginProbe
BugSnag Error Monitoring plugin / 1.6.5
BugSnag Error Monitoring plugin v1.6.5
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 1.6.5, at bugsnag.php

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