1<?php 2 3use dokuwiki\plugin\sentry\Event; 4 5/** 6 * DokuWiki Plugin sentry (Helper Component) 7 * 8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 9 * @author Andreas Gohr, Michael Große <dokuwiki@cosmocode.de> 10 */ 11class helper_plugin_sentry extends DokuWiki_Plugin 12{ 13 /** 14 * Parse the DSN configuration into its parts 15 * 16 * @return array 17 */ 18 protected function parseDSN() 19 { 20 $parts = parse_url($this->getConf('dsn')); 21 $dsn = []; 22 $dsn['protocol'] = $parts['scheme']; 23 $dsn['public'] = $parts['user']; 24 $dsn['secret'] = $parts['pass']; 25 $dsn['project'] = (int)basename($parts['path']); 26 $dsn['url'] = $parts['host']; 27 if (!empty($parts['port'])) $dsn['url'] .= ':' . $parts['port']; 28 29 $path = dirname($parts['path']); 30 $path = trim($path, '/'); 31 if (!empty($path)) { 32 $path = '/' . $path; 33 } 34 $dsn['path'] = $path; 35 36 return $dsn; 37 } 38 39 /** 40 * Return the API endpoint to store messages 41 * 42 * @return string 43 */ 44 protected function storeAPI() 45 { 46 $dsn = $this->parseDSN(); 47 return $dsn['protocol'] . '://' . $dsn['url'] . $dsn['path'] . '/api/' . $dsn['project'] . '/store/'; 48 } 49 50 /** 51 * Return the X-Sentry-Auth header 52 * 53 * @return string 54 */ 55 protected function storeAuthHeader() 56 { 57 $dsn = $this->parseDSN(); 58 59 $header[] = 'Sentry sentry_version=7'; 60 $header[] = 'sentry_client=' . Event::CLIENT . Event::VERSION; 61 $header[] = 'sentry_timestamp=' . time(); 62 $header[] = 'sentry_key=' . $dsn['public']; 63 $header[] = 'sentry_secret=' . $dsn['secret']; 64 65 return join(', ', $header); 66 } 67 68 /** 69 * Log an exception 70 * 71 * If you need more control over the logged Event, use logEvent() 72 * 73 * @param \Throwable|\Exception $e 74 */ 75 public function logException($e) 76 { 77 $this->logEvent(Event::fromException($e)); 78 79 } 80 81 /** 82 * Log an event 83 * 84 * @param Event $event 85 */ 86 public function logEvent(Event $event) 87 { 88 $this->saveEvent($event); 89 if ($this->sendEvent($event)) $this->deleteEvent($event->getID()); 90 } 91 92 93 /** 94 * Log a message and optionally some data to sentry 95 * 96 * @param string $message the raw message string 97 * @param array $extra 98 */ 99 public function logMessage($message, array $extra = []) 100 { 101 $backtrace = debug_backtrace(); 102 array_shift($backtrace); // remove this logMessage method 103 104 $eventData = [ 105 'sentry.interfaces.Message' => [ 106 'message' => $message, 107 ], 108 'stacktrace' => ['frames' => Event::backTraceFrames($backtrace)], 109 'extra' => $extra, 110 ]; 111 112 $event = new Event($eventData); 113 $event->setLogLevel('info'); 114 $this->logEvent($event); 115 } 116 117 /** 118 * Format an exception for the user in HTML 119 * 120 * @param \Throwable|\Exception $e 121 * @return string the HTML 122 */ 123 public function formatException($e) 124 { 125 global $conf; 126 $html = '<div style="width:60%; margin: auto; background-color: #fcc; 127 border: 1px solid #faa; padding: 0.5em 1em; font-family: sans-serif">'; 128 $html .= '<h1>An error occured</h1>'; 129 $html .= '<p>' . hsc(get_class($e)) . ': ' . $e->getMessage() . '</p>'; 130 if ($conf['allowdebug']) { 131 $html .= '<p><code>' . hsc($e->getFile()) . ':' . hsc($e->getLine()) . '</code></p>'; 132 $html .= '<pre>' . hsc($e->getTraceAsString()) . '</pre>'; 133 } 134 $html .= '<p>The error has been logged.</p>'; 135 $html .= '</div>'; 136 137 return $html; 138 } 139 140 /** 141 * Save the given event to file system 142 * 143 * @param Event $event 144 */ 145 public function saveEvent(Event $event) 146 { 147 global $conf; 148 $cachedir = $conf['cachedir'] . '/_sentry/'; 149 $file = $cachedir . $event->getID() . '.json'; 150 io_makeFileDir($file); 151 file_put_contents($file, $event->getJSON()); 152 } 153 154 /** 155 * Load a pending event 156 * 157 * @param string $id 158 * @return Event|null 159 */ 160 public function loadEvent($id) 161 { 162 global $conf; 163 $cachedir = $conf['cachedir'] . '/_sentry/'; 164 $file = $cachedir . $id . '.json'; 165 if (!file_exists($file)) return null; 166 $json = file_get_contents($file); 167 return Event::fromJSON($json); 168 } 169 170 /** 171 * Delete a pending event 172 * 173 * @param string $id 174 */ 175 public function deleteEvent($id) 176 { 177 global $conf; 178 $cachedir = $conf['cachedir'] . '/_sentry/'; 179 $file = $cachedir . $id . '.json'; 180 // the event may have been deleted in the meantime 181 @unlink($file); 182 } 183 184 /** 185 * Returns a list of event IDs that have not yet been sent 186 * 187 * @return string[] 188 */ 189 public function getPendingEventIDs() 190 { 191 global $conf; 192 $cachedir = $conf['cachedir'] . '/_sentry/'; 193 194 $files = glob($cachedir . '/*.json'); 195 return array_map(function ($in) { 196 return basename($in, '.json'); 197 }, $files); 198 } 199 200 /** 201 * Send the given event to sentry 202 * 203 * You most probably want to use logEvent() or logException() instead 204 * 205 * @param Event $event the event 206 * @return bool was the event submitted successfully? 207 */ 208 public function sendEvent(Event $event) 209 { 210 if (class_exists('dokuwiki\HTTP\DokuHTTPClient')) { 211 $http = new dokuwiki\HTTP\DokuHTTPClient(); 212 } else { 213 $http = new DokuHTTPClient(); 214 } 215 $http->timeout = 4; // this should not take long! 216 $http->headers['User-Agent'] = Event::CLIENT . Event::VERSION; 217 $http->headers['X-Sentry-Auth'] = $this->storeAuthHeader(); 218 $http->headers['Content-Type'] = 'application/json'; 219 $ok = $http->post($this->storeAPI(), $event->getJSON()); 220 if (!$ok) dbglog($http->resp_body, 'Sentry returned Error'); 221 return (bool)$ok; 222 } 223 224 /** 225 * Return the wanted error reporting 226 * 227 * @return int 228 */ 229 public function errorReporting() { 230 $conf = (int) $this->getConf('errors'); 231 if($conf === 0) return error_reporting(); 232 return $conf; 233 } 234} 235 236