1 <?php
2 
3 use 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  */
11 class 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