Completed
Pull Request — master (#1959)
by Basil
03:01 queued 51s
created

ErrorHandlerTrait::buildTrace()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace luya\traits;
4
5
use Yii;
6
use yii\helpers\Json;
7
use Curl\Curl;
8
use luya\helpers\Url;
9
use luya\helpers\ObjectHelper;
10
use luya\helpers\ArrayHelper;
11
use yii\web\Application;
12
use yii\web\HttpException;
13
use yii\base\Exception;
14
use yii\base\ErrorException;
15
use luya\Boot;
16
use luya\exceptions\WhitelistedException;
17
18
/**
19
 * ErrorHandler trait to extend the renderException method with an api call if enabled.
20
 *
21
 * @author Basil Suter <[email protected]>
22
 * @since 1.0.0
23
 */
24
trait ErrorHandlerTrait
25
{
26
    /**
27
     * @var string The url of the error api without trailing slash. Make sure you have installed the error api
28
     * module on the requested api url (https://luya.io/guide/module/luyadev---luya-module-errorapi).
29
     *
30
     * An example when using the erroapi module, the url could look like this `https://luya.io/errorapi`.
31
     */
32
    public $api;
33
34
    /**
35
     * @var boolean Enable the transfer of exceptions to the defined `$api` server.
36
     */
37
    public $transferException = false;
38
39
    /**
40
     * @var \Curl\Curl|null The curl object from the last error api call.
41
     * @since 1.0.5
42
     */
43
    public $lastTransferCall;
44
    
45
    /**
46
     * @var array An array of exceptions which are whitelisted and therefore NOT TRANSFERED.
47
     * @since 1.0.5
48
     */
49
    public $whitelist = [
50
        'yii\base\InvalidRouteException',
51
        'yii\web\NotFoundHttpException',
52
        'yii\web\ForbiddenHttpException',
53
        'yii\web\MethodNotAllowedHttpException',
54
        'yii\web\UnauthorizedHttpException',
55
        'yii\web\BadRequestHttpException',
56
    ];
57
    
58
    /**
59
     * @var array
60
     * @since 1.0.6
61
     */
62
    public $sensitiveKeys = ['password', 'pwd', 'pass', 'passwort', 'pw', 'token', 'hash', 'authorization'];
63
    
64
    /**
65
     * Send a custom message to the api server event its not related to an exception.
66
     *
67
     * Sometimes you just want to pass informations from your application, this method allows you to transfer
68
     * a message to the error api server.
69
     *
70
     * Example of sending a message
71
     *
72
     * ```php
73
     * Yii::$app->errorHandler->transferMessage('Something went wrong here!', __FILE__, __LINE__);
74
     * ```
75
     *
76
     * @param string $message The message you want to send to the error api server.
77
     * @param string $file The you are currently send the message (use __FILE__)
78
     * @param string $line The line you want to submit (use __LINE__)
79
     * @return bool|null
80
     */
81
    public function transferMessage($message, $file = __FILE__, $line = __LINE__)
82
    {
83
        return $this->apiServerSendData($this->getExceptionArray([
84
            'message' => $message,
85
            'file' => $file,
86
            'line' => $line,
87
        ]));
88
    }
89
90
    /**
91
     * Returns whether a given exception is whitelisted or not.
92
     * 
93
     * If an exception is whitelisted or in the liste of whitelisted exception
94
     * the exception content won't be transmitted to the error api.
95
     *
96
     * @param mixed $exception
97
     * @return boolean Returns true if whitelisted, or false if not and can therefore be transmitted.
98
     * @since 1.0.21
99
     */
100
    public function isExceptionWhitelisted($exception)
101
    {
102
        if (!is_object($exception)) {
103
            return false;
104
        }
105
106
        if (ObjectHelper::isInstanceOf($exception, WhitelistedException::class, false)) {
107
            return true;
108
        }
109
110
        return ObjectHelper::isInstanceOf($exception, $this->whitelist, false);
111
    }
112
    
113
    /**
114
     * Send the array data to the api server.
115
     *
116
     * @param array $data The array to be sent to the server.
117
     * @return boolean|null true/false if data has been sent to the api successfull or not, null if the transfer is disabled.
118
     */
119
    private function apiServerSendData(array $data)
120
    {
121
        $curl = new Curl();
122
        $curl->setOpt(CURLOPT_CONNECTTIMEOUT, 2);
123
        $curl->setOpt(CURLOPT_TIMEOUT, 2);
124
        $curl->post(Url::ensureHttp(rtrim($this->api, '/')).'/create', [
125
            'error_json' => Json::encode($data),
126
        ]);
127
        $this->lastTransferCall = $curl;
128
        
129
        return $curl->isSuccess();
130
    }
131
    
132
    /**
133
     * @inheritdoc
134
     */
135
    public function renderException($exception)
136
    {
137
        if ($this->transferException && !$this->isExceptionWhitelisted($exception)) {
138
            $this->apiServerSendData($this->getExceptionArray($exception));
139
        }
140
        
141
        return parent::renderException($exception);
142
    }
143
144
    /**
145
     * Get an readable array to transfer from an exception
146
     *
147
     * @param mixed $exception Exception object
148
     * @return array An array with transformed exception data
149
     */
150
    public function getExceptionArray($exception)
151
    {
152
        $_message = 'Uknonwn exception object, not instance of \Exception.';
153
        $_file = 'unknown';
154
        $_line = 0;
155
        $_trace = [];
156
        $_previousException = [];
157
        $_exceptionClassName = 'Unknown';
158
        
159
        if (is_object($exception)) {
160
            $_exceptionClassName = get_class($exception);
161
            $prev = $exception->getPrevious();
162
            
163
            if (!empty($prev)) {
164
                $_previousException = [
165
                    'message' => $prev->getMessage(),
166
                    'file' => $prev->getFile(),
167
                    'line' => $prev->getLine(),
168
                    'trace' => $this->buildTrace($prev),
169
                ];
170
            }
171
            
172
            $_trace = $this->buildTrace($exception);
173
            $_message = $exception->getMessage();
174
            $_file = $exception->getFile();
175
            $_line = $exception->getLine();
176
        } elseif (is_string($exception)) {
177
            $_message = 'exception string: ' . $exception;
178
        } elseif (is_array($exception)) {
179
            $_message = isset($exception['message']) ? $exception['message'] : 'exception array dump: ' . print_r($exception, true);
180
            $_file = isset($exception['file']) ? $exception['file'] : __FILE__;
181
            $_line = isset($exception['line']) ? $exception['line'] : __LINE__;
182
        }
183
184
        $exceptionName = 'Exception';
185
186
        if ($exception instanceof Exception) {
187
            $exceptionName = $exception->getName();
188
        } elseif ($exception instanceof ErrorException) {
189
            $exceptionName = $exception->getName();
190
        }
191
192
        return [
193
            'message' => $_message,
194
            'file' => $_file,
195
            'line' => $_line,
196
            'requestUri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null,
197
            'serverName' => isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null,
198
            'date' => date('d.m.Y H:i'),
199
            'trace' => $_trace,
200
            'previousException' => $_previousException,
201
            'ip' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null,
202
            'get' => isset($_GET) ? ArrayHelper::coverSensitiveValues($_GET, $this->sensitiveKeys) : [],
203
            'post' => isset($_POST) ? ArrayHelper::coverSensitiveValues($_POST, $this->sensitiveKeys) : [],
204
            'bodyParams' => Yii::$app instanceof Application ? ArrayHelper::coverSensitiveValues(Yii::$app->request->bodyParams) : [],
205
            'session' => isset($_SESSION) ? ArrayHelper::coverSensitiveValues($_SESSION, $this->sensitiveKeys) : [],
206
            'server' => isset($_SERVER) ? ArrayHelper::coverSensitiveValues($_SERVER, $this->sensitiveKeys) : [],
207
            // since 1.0.20
208
            'yii_debug' => YII_DEBUG,
209
            'yii_env' => YII_ENV,
210
            'status_code' => $exception instanceof HttpException ? $exception->statusCode : 500,
211
            'exception_name' => $exceptionName,
212
            'exception_class_name' => $_exceptionClassName,
213
            'php_version' => phpversion(),
214
            'luya_version' => Boot::VERSION,
215
        ];
216
    }
217
    
218
    /**
219
     * Build trace array from exception.
220
     *
221
     * @param object $exception
222
     * @return array
223
     */
224
    private function buildTrace($exception)
225
    {
226
        $_trace = [];
227
        foreach ($exception->getTrace() as $key => $item) {
228
            $_trace[$key] = $this->buildTraceItem($item);
229
        }
230
        
231
        return $_trace;
232
    }
233
234
    /**
235
     * Build the array trace item with file context.
236
     *
237
     * @param array $item
238
     * @return array
239
     */
240
    private function buildTraceItem(array $item)
241
    {
242
        $file = isset($item['file']) ? $item['file'] : null;
243
        $line = isset($item['line']) ? $item['line'] : null;
244
        $contextLine = null;
245
        $preContext = [];
246
        $postContext = [];
247
248
        if (!empty($file)) {
249
            try {
250
                $lineInArray = $line -1;
251
                // load file from path
252
                $fileInfo = file($file, FILE_IGNORE_NEW_LINES);
253
                // load file if false from real path
254
                if (!$fileInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fileInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
255
                    $fileInfo = file(realpath($file), FILE_IGNORE_NEW_LINES);
256
                }
257
                if ($fileInfo && isset($fileInfo[$lineInArray])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fileInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
258
                    $contextLine = $fileInfo[$lineInArray];
259
                }
260
                if ($contextLine) {
261
                    for ($i = 1; $i <= 6; $i++) {
262
                        // pre context
263
                        if (isset($fileInfo[$lineInArray - $i])) {
264
                            $preContext[] = $fileInfo[$lineInArray - $i];
265
                        }
266
                        // post context
267
                        if (isset($fileInfo[$i + $lineInArray])) {
268
                            $postContext[] = $fileInfo[$i + $lineInArray];
269
                        }
270
                    }
271
                    $preContext = array_reverse($preContext);
272
                }
273
                unset($fileInfo);
274
            } catch (\Exception $e) {
275
                // catch if any file load error appears
276
            }
277
        }
278
279
        return array_filter([
280
            'file' => $file,
281
            'abs_path' => realpath($file),
282
            'line' => $line,
283
            'context_line' => $contextLine,
284
            'pre_context' => $preContext,
285
            'post_context' => $postContext,
286
            'function' => isset($item['function']) ? $item['function'] : null,
287
            'class' => isset($item['class']) ? $item['class'] : null,
288
            // currently arguments wont be transmited due to large amount of informations based on base object
289
            //'args' => isset($item['args']) ? ArrayHelper::coverSensitiveValues($item['args'], $this->sensitiveKeys) : [],
290
        ], function($value) {
291
            return !empty($value);
292
        });
293
    }
294
}
295