AbstractException.php 12.1 KB
Newer Older
1
2
3
4
5
6
7
8
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/29/16
 * Time: 8:03 AM
 */

Marc Egger's avatar
Marc Egger committed
9
namespace IMATHUZH\Qfq\Core\Exception;
10

Marc Egger's avatar
Marc Egger committed
11
12
13
14
15
16
17
18
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\T3Info;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\Support;
19

20

21
22
/**
 * Class AbstractException
23
24
25
 *
 * Throw with ONE message
 *
Marc Egger's avatar
Marc Egger committed
26
 *   throw new \UserFormException('Failed: chmod ....', ERROR_IO_CHMOD);
27
28
29
 *
 * Throw with message for User and message for Support.
 *
Marc Egger's avatar
Marc Egger committed
30
  throw new \UserFormException(  json_encode(
31
32
33
34
                                   [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
                                    ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'",
                                    ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]),
                                ERROR_IO_CHMOD);
35
 *
36
37
 * @package qfq
 */
38
39
40
class AbstractException extends \Exception {

    public $messageArray = array();
41
42
    public $messageArrayDebug = [EXCEPTION_MESSAGE_DEBUG => ''];

43
    public $store = null;
44

45
46
47
    protected $file = '';
    protected $line = '';

48
49
    protected $httpStatusCode = '400 Bad Request';

50
    /**
51
52
53
     * $this->getMessage() might give
     *   a) a simple string, or
     *   b) an JSON String.
54
     *
55
     * If it is a JSON String: There are 3+1 different messages:
56
57
58
59
     *   [ERROR_MESSAGE_TO_USER] 'toUser' - shown in the client to the user - no details here!!!
     *   [ERROR_MESSAGE_SUPPORT] 'support' - help for the developer
     *   [ERROR_MESSAGE_OS] 'os' - message from the OS, like 'file not found'
     *
60
61
     * Stacktrace, Form, FormElement, Report level, T3 page, T3 tt_content uid, ...
     *
62
     * @return string
Marc Egger's avatar
Marc Egger committed
63
64
     * @throws \CodeException
     * @throws \UserFormException
65
66
     */
    public function formatException() {
67

68
        $t3Vars = array();
69
70
        $arrShow = $this->messageArray;
        $htmlDebug = '';
71
        $arrDebugShow = array();
72
        $arrForm = [];
73

74
75
76
        try {
            // In a very early stage, it might be possible that Store can't be initialized: take care not to use it.
            $store = Store::getInstance();
77
            $t3Vars = $store->getStore(STORE_TYPO3);
Marc Egger's avatar
Marc Egger committed
78
        } catch (\CodeException $e) {
79
            $store = null;
80
        } catch (\Exception $exception) {
81
            $store = null;
82
83
        }

84
85
86
        if (empty($t3Vars)) {
            $t3Vars = T3Info::getVars();
        }
87

88
89
        $arrShow[EXCEPTION_TIMESTAMP] = date('Y.m.d H:i:s O');
        $arrShow[EXCEPTION_CODE] = $this->getCode();
90
91
92
93
94
95
        $arrShow[EXCEPTION_UNIQID] = uniqid();

        // Get exception message and if JSON, decode it.
        $msg = $this->getMessage();
        $arrMsg = json_decode($msg, true);
        if ($arrMsg === null) {
96

97
98
            $arrShow[EXCEPTION_MESSAGE] = $msg;
            $arrMsg[ERROR_MESSAGE_TO_USER] = $msg;
99

100
101
        } else {
            $arrShow[EXCEPTION_MESSAGE] = $arrMsg[ERROR_MESSAGE_TO_USER];
102
103
104
105
106

            if (isset($arrMsg[ERROR_MESSAGE_HTTP_STATUS])) {
                $this->httpStatusCode = $arrMsg[ERROR_MESSAGE_HTTP_STATUS];
            }

107
        }
108

109
110
        $arrDebugHidden[EXCEPTION_FILE] = $this->getFile();
        $arrDebugHidden[EXCEPTION_LINE] = $this->getLine();
111

112
113
        $arrTrace = $this->getExtensionTraceAsArray();
        if ($store !== null) {
114

115
            $this->messageArrayDebug[EXCEPTION_MESSAGE_DEBUG] = Store::getVar(EXCEPTION_MESSAGE_DEBUG, STORE_SYSTEM);
116

117
            $arrDebugShow = array_merge([EXCEPTION_REPORT_FULL_LEVEL => Store::getVar(SYSTEM_REPORT_FULL_LEVEL, STORE_SYSTEM)], $this->messageArrayDebug);
118

119
            $arrDebugShow[EXCEPTION_SIP] = $store->getStore(STORE_SIP);
120
121
122
            $arrDebugShow[EXCEPTION_PAGE_ID] = $t3Vars[TYPO3_PAGE_ID] ?? '-';
            $arrDebugShow[EXCEPTION_TT_CONTENT_UID] = $t3Vars[TYPO3_TT_CONTENT_UID] ?? '-';
            $arrDebugShow[EXCEPTION_FE_USER] = $t3Vars[TYPO3_FE_USER] ?? '-';
123
            $arrDebugShow[EXCEPTION_FE_USER_STORE_USER] = Store::getVar(TYPO3_FE_USER, STORE_USER);
124

125
            // Optional existing arrays will be flattened
126
127
            $arrDebugShow = OnArray::varExportArray($arrDebugShow);
            $arrDebugHidden = OnArray::varExportArray($arrDebugHidden);
128

129
            $arrDebugHidden[EXCEPTION_IP_ADDRESS] = $store->getVar(CLIENT_REMOTE_ADDRESS, STORE_CLIENT);
130
131
132
            // No need for this information:
            // $arrDebugHidden[EXCEPTION_HTTP_USER_AGENT] = $store->getVar(CLIENT_HTTP_USER_AGENT, STORE_CLIENT, SANITIZE_ALLOW_ALLBUT);
            // $arrDebugHidden[EXCEPTION_QFQ_COOKIE] = $store->getVar(CLIENT_COOKIE_QFQ, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
133

134
135
            // Debug Information
            if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {
136

137
138
139
140
                // In case there is a 'form' name given in SIP, we probably have a problem in a form and a direct link to
                // edit the broken form will be helpful.
                $storeSystem = $store->getStore(STORE_SYSTEM);
                if (!empty($storeSystem[SYSTEM_FORM])) {
141
142
                    $arrForm['Edit'] = $this->buildFormLink($storeSystem);
                    $arrForm['FE column'] = Store::getVar(SYSTEM_FORM_ELEMENT_COLUMN, STORE_SYSTEM);
143
                }
144

145
                //  Check if the 'developer message shouldn't be sanitized
Marc Egger's avatar
Marc Egger committed
146
147
148
                $developerRaw = null;
                if (($arrMsg[ERROR_MESSAGE_TO_DEVELOPER_SANITIZE] ?? true) == false) {
                    $developerRaw = $arrMsg[ERROR_MESSAGE_TO_DEVELOPER] ?? '';
149
150
151
                }
                unset($arrMsg[ERROR_MESSAGE_TO_DEVELOPER_SANITIZE]);

152
                $arrMerged = OnArray::htmlentitiesOnArray(array_merge($arrMsg, $arrDebugShow));
153
                // Restore raw developer message
Marc Egger's avatar
Marc Egger committed
154
155
                if ($developerRaw !== null) {
                    $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] = $developerRaw;
156
                }
157

158
                if (!empty($os = $arrMerged[ERROR_MESSAGE_OS] ?? '')) {
159
                    // [ mysqli: 1146 ] Table 'qfq_db.UNKNOWN_TABLE' doesn't exist
160
                    $before = $this->getTableToken(html_entity_decode($arrMerged[ERROR_MESSAGE_OS], ENT_QUOTES));
161
162
                    $arrMerged[EXCEPTION_SQL_FINAL] = $this->sqlHighlightError($arrMerged[ERROR_MESSAGE_OS], 'mysqli: 1146', $arrMerged[EXCEPTION_SQL_FINAL], $before, "' doesn't exist");
                    $arrMerged[EXCEPTION_SQL_FINAL] = $this->sqlHighlightError($arrMerged[ERROR_MESSAGE_OS], 'mysqli: 1064', $arrMerged[EXCEPTION_SQL_FINAL], "the right syntax to use near '", "' at line [0-9]*$");
163
164
                    // [ mysqli: 1054 ] Unknown column "noPsp.pspElement' in 'field list" | "... in 'order clause'"
                    $arrMerged[EXCEPTION_SQL_FINAL] = $this->sqlHighlightError($arrMerged[ERROR_MESSAGE_OS], 'mysqli: 1054', $arrMerged[EXCEPTION_SQL_FINAL], "Unknown column '", "' in '");
165
                }
166

167
168
169
170
                foreach ($arrMerged as $key => $value) {
                    $arrMerged[$key] = str_replace("\n", "<br>", $arrMerged[$key]);
                }

171
                $htmlDebug = OnArray::arrayToHtmlTable(
172
173
                    array_merge($arrForm, $arrMerged),
                    'Debug', EXCEPTION_TABLE_CLASS);
174

175
176
                $arrDebugHiddenClean = OnArray::htmlentitiesOnArray($arrDebugHidden);
                $arrDebugHiddenClean[EXCEPTION_STACKTRACE] = implode($arrTrace, '<br>');
177
//                $arrDebugHiddenClean[EXCEPTION_EDIT_FORM] = implode($arrTrace, '<br>');
178
179
180
181
182
183
                $hidden = OnArray::arrayToHtmlTable($arrDebugHiddenClean, 'Details', EXCEPTION_TABLE_CLASS);

                // Show / hide with just CSS: http://jsfiddle.net/t5Nf8/1/
                $htmlDebug .= "<style>input[type=checkbox]:checked + label + table { display: none; }</style>" .
                    "<input type='checkbox' checked id='stacktrace'><label for='stacktrace'>&nbsp;Show/hide more details</label>$hidden";
            }
184
185
        }

186
        $qfqLog = ($store == null) ? SYSTEM_QFQ_LOG_FILE : $store->getVar(SYSTEM_QFQ_LOG, STORE_SYSTEM);
187
188
189
190
        $arrDebugHidden[EXCEPTION_STACKTRACE] = PHP_EOL . implode($arrTrace, PHP_EOL);
        $arrLogAll = array_merge($arrMsg, $arrShow, $arrDebugShow, $arrDebugHidden);
        $logAll = OnArray::arrayToLog($arrLogAll);
        Logger::logMessage($logAll, $qfqLog);
191

192
193
        // Sanitize any HTML content.
        $arrShow = OnArray::htmlentitiesOnArray($arrShow);
194

195
        return $this->formatMessageUser($arrShow) . $htmlDebug;
196
197
198

    }

199
200
201
202
    /**
     * @return string
     */
    public function getHttpStatus() {
203
        return $this->httpStatusCode;
204
205
    }

206
    /**
207
208
209
210
     * Extract 'beforeMatch', incl. dynamic db name as token to do underlining later.
     * E.g.:  "[ mysqli: 1146 ] Table 'qfq_db.UNKNOWN_TABLE' doesn't exist"
     * return: "Table 'qfq_db."
     *
211
212
213
214
215
216
     * @param $os
     * @return string
     */
    private function getTableToken($os) {
        $subject = "Table '.*' ";
        $arr = preg_match("/$subject/", $os, $matches);
217
218
        $arr = explode('.', $matches[0] ?? '');
        return ($arr[0] ?? '') . '.';
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
    }

    /**
     * @param $os
     * @param $code
     * @param $sql
     * @param $before
     * @param $after
     * @return string
     */
    private function sqlHighlightError($os, $code, $sql, $before, $after) {

        $beforeMatch = htmlentities($before, ENT_QUOTES);
        $afterMatch = htmlentities($after, ENT_QUOTES);
        if (preg_match("/$code.*$beforeMatch.*$afterMatch/", $os)) {
            $arr = explode("$beforeMatch", $os, 2);
            $match = $arr[1] ?? '';
            $match = preg_split("/$afterMatch/", $match)[0];

            if (!empty($match)) {
                $splitSql = explode($match, $sql);
                $match = Support::wrapTag('<span class="qfq-wavy-underline">', $match);
                $highlightedSql = implode($match, $splitSql);
                $sql = $highlightedSql;
            }
        }
        return $sql;
246
    }
247

248
249
250
251
252
253
254
255
256
    /**
     * @return array
     */
    private function getExtensionTraceAsArray() {

        $trace = $this->getTraceAsString();
        $arrTrace = explode(PHP_EOL, $trace);

        return OnArray::filterValueSubstring($arrTrace, '/typo3conf/ext/' . EXT_KEY . '/');
257
    }
258

259
260
261
262
263
264
265
266
    /**
     * @param $arrShow
     * @return string
     */
    private function formatMessageUser($arrShow) {

        $html = '<p><em>' . $arrShow[EXCEPTION_TIMESTAMP] . ', Reference: ' . $arrShow[EXCEPTION_UNIQID] . '</em></p>';
        $html .= '<p>' . $arrShow[EXCEPTION_MESSAGE] . '</p>';
267

268
269
        return $html;
    }
270

271
272
    /**
     * Build a FormEditor link to the broken form.
273
274
     * @param $storeSystem
     * @return string
275
276
277
278
279
280
281
     */
    private function buildFormLink($storeSystem) {

        $linkForm = '';
        $linkFormElement = '';
        try {
            $db = new Database();
282
            $sql = "SELECT `id` FROM `Form` WHERE `name`='" . $storeSystem[SYSTEM_FORM] . "'";
283
284
285
286
287

            $r = $db->sql($sql, ROW_EXPECT_0_1);
            if (!is_numeric($r[F_ID])) {
                return '';
            }
288

289
290
291
292
            $sip = new Sip();
            $link = new Link($sip);

            // Link to 'Form'
293
294
            $linkForm = $link->renderLink(TOKEN_SIP . '|' . TOKEN_BOOTSTRAP_BUTTON . '|' . TOKEN_PAGE . ':' .
                $storeSystem[SYSTEM_EDIT_FORM_PAGE] . '&' . CLIENT_FORM . '=' . FORM_NAME_FORM . '&' .
295
296
297
298
                CLIENT_RECORD_ID . '=' . $r[F_ID] . '|' . TOKEN_TEXT . ':' . $storeSystem[SYSTEM_FORM]);

            // Link to 'FormElement'
            if (!empty($storeSystem[SYSTEM_FORM_ELEMENT_ID])) {
299
300
301
                $linkFormElement = $link->renderLink(TOKEN_SIP . '|' . TOKEN_BOOTSTRAP_BUTTON . '|' . TOKEN_PAGE .
                    ':' . $storeSystem[SYSTEM_EDIT_FORM_PAGE] . '&' . CLIENT_FORM . '=' . FORM_NAME_FORM_ELEMENT . '&' .
                    CLIENT_RECORD_ID . '=' . $storeSystem[SYSTEM_FORM_ELEMENT_ID] . '|' .
302
                    TOKEN_TEXT . ':' . $storeSystem[SYSTEM_FORM_ELEMENT]);
303
            }
304

305
        } catch (\exception $e) {
306
            // none, should rise up
307
            return "Error build direct 'Form-Edit-Link'";
308
        }
Carsten  Rose's avatar
Carsten Rose committed
309

310
        return 'Form: ' . $linkForm . '&nbsp;&nbsp;  FormElement: ' . $linkFormElement;
311
312
    }
}