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