QuickFormQuery.php 88.5 KB
Newer Older
1
2
3
4
5
6
7
8
<?php
/**
 * Created by PhpStorm.
 * User: ep
 * Date: 12/23/15
 * Time: 6:33 PM
 */

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

require __DIR__ . '/../../vendor/autoload.php';

13
14
15
16
17
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Database\DatabaseUpdate;
use IMATHUZH\Qfq\Core\Form\Dirty;
use IMATHUZH\Qfq\Core\Form\DragAndDrop;
use IMATHUZH\Qfq\Core\Form\FormAction;
Marc Egger's avatar
Marc Egger committed
18
use IMATHUZH\Qfq\Core\Form\FormAsFile;
Marc Egger's avatar
Marc Egger committed
19
use IMATHUZH\Qfq\Core\Helper\HelperFile;
20
21
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\KeyValueStringParser;
Marc Egger's avatar
Marc Egger committed
22
23
24
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Support;
25
26
use IMATHUZH\Qfq\Core\Report\Monitor;
use IMATHUZH\Qfq\Core\Report\Report;
27
use IMATHUZH\Qfq\Core\Report\ReportAsFile;
Marc Egger's avatar
Marc Egger committed
28
29
use IMATHUZH\Qfq\Core\Store\FillStoreForm;
use IMATHUZH\Qfq\Core\Store\Session;
30
31
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
Marc Egger's avatar
Marc Egger committed
32

33
34
35
36
37
38
39
40
41
42
43
44
/*
 * Form will be called
 * a) with a SIP identifier, or
 * b) without a SIP identifier (form setting has to allow this) and will create on the fly a new SIP.
 *
 * The SIP-Store stores:
 *  form=<formname>
 *  r=<record id>  (table.id for a single record form)
 *  keySemId,keySemIduser
 *  <further individual variables>
 */

Carsten  Rose's avatar
Carsten Rose committed
45
/**
46
 * Class Qfq
Carsten  Rose's avatar
Carsten Rose committed
47
48
 * @package qfq
 */
49
class QuickFormQuery {
50

51
    /**
52
     * @var Store instantiated class
53
     */
Carsten  Rose's avatar
Carsten Rose committed
54
    protected $store = null;
55

56
    /**
Marc Egger's avatar
Marc Egger committed
57
     * @var Database[] - Array of Database instantiated class
58
     */
59
    protected $dbArray = array();
60

61
62
63
    /**
     * @var Evaluate instantiated class
     */
64
65
    protected $evaluate = null;

66
    protected $formSpec = array(); // Stores the form content after parsing SQL queries and QFQ syntax stored in form attributes
67
68
    protected $feSpecAction = array();  // Form Definition: copy of the loaded form
    protected $feSpecNative = array(); // FormEelement Definition: all formElement.class='action' of the loaded form
69
    protected $feSpecNativeRaw = array(); // FormEelement Definition: all formElement.class='action' of the loaded form
70

71
72
73
    /**
     * @var array
     */
74
    private $t3data = array(); // FormElement Definition: all formElement.class='native' of the loaded form
75

76
77
78
    /**
     * @var bool
     */
79
80
    private $phpUnit = false;

81
82
83
84
85
    /**
     * @var bool
     */
    private $inlineReport = false;

86
87
88
89
90
    /**
     * @var Session
     */
    private $session = null;

91
92
93
    private $dbIndexData = false;
    private $dbIndexQfq = false;

94
95
96
97
98
99
100
101
102
103
104
    /*
     * TODO:
     *  Preparation: setup logging, database access, record locking
     *  fill stores
     *  Check permission_create / permission_update
     *  Multi: iterate over all records, Single: activate record
     *      Check mode: Load | Save
     *      doActions 'Before'
     *      Do all FormElements
     *      doActions 'After'
     */
105

106
107
108
    /**
     * Construct the Form Class and Store too. This is the base initialization moment.
     *
109
110
     * As a result of instantiating of Form, the class Store will initially called the first time and therefore
     * instantiated automatically. Store might throw an exception, in case the URL-passed SIP is invalid.
111
     *
112
     * @param array $t3data
Carsten  Rose's avatar
Carsten Rose committed
113
     * @param bool $phpUnit
114
     * @param bool $inlineReport
115
     *
Marc Egger's avatar
Marc Egger committed
116
117
118
119
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
120
     */
121
    public function __construct(array $t3data = array(), $phpUnit = false, $inlineReport = true) {
122
123

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
124
        $this->phpUnit = $phpUnit;
125
        $this->inlineReport = $inlineReport;
126

127
        mb_internal_encoding("UTF-8");
128

129
130
        $this->session = Session::getInstance($phpUnit);

131
        // Refresh the session even if no new data saved.
132
        Session::set(SESSION_LAST_ACTIVITY, time());
133

134
135
        Support::setQfqErrorHandler();

Carsten  Rose's avatar
Carsten Rose committed
136
137
        // PHPExcel
        set_include_path(get_include_path() . PATH_SEPARATOR . '../../Resources/Private/Classes/');
138

139
        if (!isset($t3data[T3DATA_BODYTEXT])) {
140
            // TODO: ?CR: when does this happen? why no exception thrown?
141
142
143
144
            $t3data[T3DATA_BODYTEXT] = '';
        }

        if (!isset($t3data[T3DATA_UID])) {
145
            // TODO: ?CR: when does this happen? why no exception thrown?
146
147
            $t3data[T3DATA_UID] = 0;
        }
148

149
150
151
152
153
154
155
156
157
158
159
160
        // Read report file if file keyword exists
        $reportPathFileNameFull = ReportAsFile::parseFileKeyword($t3data[T3DATA_BODYTEXT]);
        if ($reportPathFileNameFull !== null) {
            $fileContents = file_get_contents($reportPathFileNameFull);
            if ($fileContents === false) {
                throw new \UserReportException(json_encode([
                    ERROR_MESSAGE_TO_USER => "File read error.",
                    ERROR_MESSAGE_TO_DEVELOPER => "Report file not found or no permission to read file: '$reportPathFileNameFull'"]),
                    ERROR_FORM_NOT_FOUND);
            }
            $t3data[T3DATA_BODYTEXT] = $fileContents;
        }
161

162
        $btp = new BodytextParser();
163
        $t3data[T3DATA_BODYTEXT_RAW] = $t3data[T3DATA_BODYTEXT];
164
        $t3data[T3DATA_BODYTEXT] = $btp->process($t3data[T3DATA_BODYTEXT]);
165

166
167
        $this->t3data = $t3data;

168
        $bodytext = $this->t3data[T3DATA_BODYTEXT];
169
170

        $this->store = Store::getInstance($bodytext, $phpUnit);
171

172
173
174
        $timeout = $this->store::getVar(SYSTEM_SESSION_TIMEOUT_SECONDS, STORE_SYSTEM);
        Session::checkSessionExpired($timeout);

175
        // If an FE user logs out and a different user logs in (same browser session) - the old values has to be destroyed!
176
        if (Session::getAndDestroyFlagFeUserHasChanged()) {
177
178
179
            $this->store->unsetStore(STORE_USER);
        }

180
        $this->store->setVar(TYPO3_TT_CONTENT_UID, $t3data[T3DATA_UID], STORE_TYPO3);
181

182
183
        $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);
        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
184

185
        $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData);
186

187
188
189
        if ($this->dbIndexData != $this->dbIndexQfq) {
            $this->dbArray[$this->dbIndexQfq] = new Database($this->dbIndexQfq);
        }
190

191
        $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
192

193
        $dbUpdate = $this->store->getVar(SYSTEM_DB_UPDATE, STORE_SYSTEM);
194
        $updateDb = new DatabaseUpdate($this->dbArray[$this->dbIndexQfq], $this->store);
195
        $updateDb->checkNupdate($dbUpdate);
196

197
        $this->store->FillStoreSystemBySql(); // Do this after the DB-update
198
199
200

        // Set dbIndex, evaluate any
        $dbIndex = $this->store->getVar(TOKEN_DB_INDEX, STORE_TYPO3 . STORE_EMPTY);
201
        $dbIndex = $this->evaluate->parse($dbIndex);
202
203
        $dbIndex = ($dbIndex == '') ? DB_INDEX_DEFAULT : $dbIndex;
        $this->store->setVar(TOKEN_DB_INDEX, $dbIndex, STORE_TYPO3);
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266

        // Create report file if file keyword not found
        // TODO: ?CR: $t3data might not contain the following values. Or uid could be 0.
        if ($reportPathFileNameFull === null && $t3data[T3DATA_UID] !== 0) {

            // read page path of tt-content element (use page alias if exists)
            $dbT3 = $this->store->getVar(SYSTEM_DB_NAME_T3, STORE_SYSTEM);
            $reportPath = '';
            $pid = $t3data[T3DATA_PID];
            while (intval($pid) !== 0) {
                $sql = "SELECT `pid`, `alias`, `title` FROM `$dbT3`.`pages` WHERE `uid` = ?";
                $page = $this->dbArray[$this->dbIndexData]->sql($sql, ROW_EXPECT_1,
                    [$pid], "Typo3 page not found. Uid: $pid");
                $supDir = $page['alias'] !== '' ? $page['alias'] : $page['title'];
                $reportPath = HelperFile::joinPathFilename(HelperFile::sanitizeFileName($supDir), $reportPath);
                $pid = $page['pid'];
                if (strlen($reportPath) > 4096) {
                    // Throw exception in case of infinite loop.
                    throw new \UserReportException(json_encode([
                        ERROR_MESSAGE_TO_USER => "Path too long.",
                        ERROR_MESSAGE_TO_DEVELOPER => "Report path too long: '$reportPath'"]),
                        ERROR_FORM_NOT_FOUND);
                }
            }
            $reportPathFull = HelperFile::joinPathFilename(ReportAsFile::reportPath(), $reportPath);

            // create path in filesystem if not exists
            HelperFile::createPathRecursive($reportPathFull);

            // add filename
            $fileName = HelperFile::sanitizeFileName($t3data[T3DATA_HEADER]);
            if ($fileName === null) {
                $fileName = strval($t3data[T3DATA_UID]);
            }
            $reportPathFileNameFull = HelperFile::joinPathFilename($reportPathFull, $fileName) . REPORT_FILE_EXTENSION;

            // if file exists, throw exception
            if (file_exists($reportPathFileNameFull)) {
                throw new \UserReportException(json_encode([
                    ERROR_MESSAGE_TO_USER => "Writing file failed.",
                    ERROR_MESSAGE_TO_DEVELOPER => "Can't export report to file. File already exists: '$reportPathFileNameFull'"]),
                    ERROR_FORM_NOT_FOUND);
            }

            // create file with content bodytext
            $success = file_put_contents($reportPathFileNameFull, $t3data[T3DATA_BODYTEXT_RAW]);
            if ($success === false) {
                throw new \UserFormException(json_encode([
                    ERROR_MESSAGE_TO_USER => "Writing file failed.",
                    ERROR_MESSAGE_TO_DEVELOPER => "Can't write to file '$reportPathFileNameFull'"]),
                    ERROR_IO_WRITE_FILE);
            }

            // replace tt-content bodytext with file=<path>
            $newBodytext = "file=" . HelperFile::joinPathFilename($reportPath, $fileName) . REPORT_FILE_EXTENSION;
            $sql = "UPDATE `$dbT3`.`tt_content` SET `bodytext` = ?, `tstamp` = UNIX_TIMESTAMP(NOW()) WHERE `uid` = ?";
            $this->dbArray[$this->dbIndexData]->sql($sql, ROW_REGULAR, [$newBodytext, $t3data[T3DATA_UID]]);

            // Clear cache
            // Need to truncate cf_cache_pages because it is used to restore page-specific cache
            $sql = "DELETE FROM `$dbT3`.`cf_cache_pages`";
            $this->dbArray[$this->dbIndexData]->sql($sql);
        }
Carsten  Rose's avatar
Carsten Rose committed
267
268
    }

269
    /**
270
     * Returns the defined forwardMode and set forwardPage
271
     *
272
     * @return array
Marc Egger's avatar
Marc Egger committed
273
274
     * @throws \CodeException
     * @throws \UserFormException
275
     */
276
277
    public function getForwardMode() {

278
        if (!isset($this->formSpec[F_FORWARD_PAGE])) {
Carsten  Rose's avatar
Carsten Rose committed
279
            // For QFQ inline editing: no redirect and no further processing.
280
281
282
            return [API_REDIRECT => API_ANSWER_REDIRECT_NO, API_REDIRECT_URL => ''];
        }

283
        $forwardPage = $this->formSpec[F_FORWARD_PAGE];
284

285
286
287
288
289
290
291
292
293
294
295
296
297
298
        switch ($this->formSpec[F_FORWARD_MODE]) {
            case F_FORWARD_MODE_URL_SIP:
                $forwardPage = store::getSipInstance()->queryStringToSip($forwardPage, RETURN_URL);
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL;
                break;
            case F_FORWARD_MODE_URL_SIP_SKIP_HISTORY:
                // F_FORWARD_MODE_URL_SIP is not defined in API PROTOCOL. At the moment it's only used for 'copyForm'.
                // 'copyForm' behaves better if the page is not in history.
                // An option for better implementing would be to separate SKIP History from ForwardMode. For API, it can be combined again.
                $forwardPage = store::getSipInstance()->queryStringToSip($forwardPage, RETURN_URL);
                $this->formSpec[F_FORWARD_MODE] = F_FORWARD_MODE_URL_SKIP_HISTORY;
                break;
            default:
                break;
Carsten  Rose's avatar
Carsten Rose committed
299
300
        }

301
302
        return ([
            API_REDIRECT => $this->formSpec[F_FORWARD_MODE],
Carsten  Rose's avatar
Carsten Rose committed
303
            API_REDIRECT_URL => $forwardPage,
304
        ]);
305
306
    }

307
    /**
308
     * Main entry point to display content: a) form and/or b) report
309
     *
310
     * @return string
Marc Egger's avatar
Marc Egger committed
311
312
313
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
314
315
316
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
317
318
319
320
321
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
322
     */
323
    public function process() {
324
        $html = '';
325

326
327
328
329
330
        $render = $this->store->getVar(SYSTEM_RENDER, STORE_TYPO3 . STORE_SYSTEM);
        if ($render == SYSTEM_RENDER_API && isset($GLOBALS['TYPO3_CONF_VARS'])) {
            return '';
        }

331
        if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === 'yes') {
332
333
            $htmlId = HelperFormElement::buildFormElementId($this->formSpec[F_ID], 0, 0, 0);
            $html .= Support::doTooltip($htmlId . HTML_ID_EXTENSION_TOOLTIP, $this->t3data['bodytext']);
334
335
336
        }

        $html .= $this->doForm(FORM_LOAD);
337
338
339
        if ($render == SYSTEM_RENDER_BOTH || $render == SYSTEM_RENDER_API || ($render == SYSTEM_RENDER_SINGLE && $html == '')) {
            $html .= $this->doReport();
        }
Carsten  Rose's avatar
Carsten Rose committed
340

341
        // Only needed if there are potential 'download'-links, which shall show a popup during processing of the download.
Carsten  Rose's avatar
Carsten Rose committed
342
343
344
345
        if ($this->store->getVar(SYSTEM_DOWNLOAD_POPUP, STORE_SYSTEM) == DOWNLOAD_POPUP_REQUEST) {
            $html .= $this->getModalCode();
        }

346
347
348
349
350
        // Only needed if there are 'drag and drop' elements.
        if ($this->store->getVar(SYSTEM_DRAG_AND_DROP_JS, STORE_SYSTEM) == 'true') {
            $html .= $this->getDragAndDropCode();
        }

351
        $class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
Carsten  Rose's avatar
Carsten Rose committed
352
        if ($class) {
353
            $html = Support::wrapTag("<div class='$class'>", $html);
Carsten  Rose's avatar
Carsten Rose committed
354
355
        }

356
        return $html;
357
358
    }

359
    /**
360
     * Determine the name of the language parameter field, which has to be taken to fill language specific definitions.
361
     *
Marc Egger's avatar
Marc Egger committed
362
363
     * @throws \CodeException
     * @throws \UserFormException
364
365
366
367
368
369
370
371
372
     */
    private function setParameterLanguageFieldName() {

        $typo3PageLanguage = $this->store->getVar(TYPO3_PAGE_LANGUAGE, STORE_TYPO3);
        if (empty($typo3PageLanguage)) {
            return;
        }

        foreach (['A', 'B', 'C', 'D'] as $key) {
373
            $languageIdx = SYSTEM_FORM_LANGUAGE . "$key" . "Id";
374
375
376
377
378
379
380
            if ($this->store->getVar($languageIdx, STORE_SYSTEM) == $typo3PageLanguage) {
                $this->store->setVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, 'parameterLanguage' . $key, STORE_SYSTEM);
                break;
            }
        }
    }

381
    /**
382
383
     * Creates an empty file. This indicates that the current form is in debug mode. Returns HTML element which will be
     * replaced by the logfile.
384
385
386
     *
     * @param $formName
     * @param $formLogMode
387
     * @return string
Marc Egger's avatar
Marc Egger committed
388
389
390
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
391
392
393
394
     */
    private function getFormLog($formName, $formLogMode) {

        $formLogFileName = Support::getFormLogFileName($formName, $formLogMode);
395
        file_put_contents($formLogFileName, '');
396

397
398
        $monitor = new Monitor();

399
400
        return "<pre id='" . FORM_LOG_HTML_ID . "'>Please wait</pre>" .
            $monitor->process([TOKEN_L_FILE => $formLogFileName, TOKEN_L_APPEND => '1', TOKEN_L_HTML_ID => FORM_LOG_HTML_ID]);
401
    }
402

403
    /**
404
     * Process form.
405
406
407
408
409
     * $mode=
     *   FORM_LOAD: The whole form will be rendered as HTML Code, including the values of all form elements
     *   FORM_UPDATE: States and values of all form elements will be returned as JSON.
     *   FORM_SAVE: The submitted form will be saved. Return Failure or Success as JSON.
     *   FORM_DELETE:
410
     *
411
     * @param string $formMode FORM_LOAD | FORM_UPDATE | FORM_SAVE | FORM_DELETE
412
     *
413
     * @return array|string
Marc Egger's avatar
Marc Egger committed
414
415
416
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
Carsten  Rose's avatar
Carsten Rose committed
417
418
419
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
420
421
422
423
424
     * @throws \Twig\Error\LoaderError
     * @throws \Twig\Error\RuntimeError
     * @throws \Twig\Error\SyntaxError
     * @throws \UserFormException
     * @throws \UserReportException
425
     */
426
    private function doForm($formMode) {
Carsten  Rose's avatar
Carsten Rose committed
427
        $data = '';
Carsten  Rose's avatar
Carsten Rose committed
428
        $foundInStore = '';
429
        $flagApiStructureReGroup = true;
430
        $formModeNew = '';
431
        $build = null;
432

433
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3 . STORE_CLIENT . STORE_ZERO, SANITIZE_ALLOW_DIGIT, $foundInStore);
434
        $this->setParameterLanguageFieldName();
435

436
437
438
        $formName = $this->loadFormSpecification($formMode, $recordId, $foundInStore, $formLogMode);
        if ($formName !== false && $formLogMode !== false) {
            return $this->getFormLog($formName, $formLogMode);
439
440
        }

441
        if ($formName === false) {
442
            switch ($formMode) {
443
                case FORM_DELETE:
444
                    $formModeNew = FORM_DELETE;
445
446
                    break;
                case FORM_DRAG_AND_DROP:
Marc Egger's avatar
Marc Egger committed
447
                    throw new \CodeException('Missing form in SIP', ERROR_MISSING_FORM);
448
449
450
                default:
                    return '';// No form found: do nothing
            }
451
        }
452

Carsten  Rose's avatar
Carsten Rose committed
453
        // Check 'session expire' happens quite late, cause it can be configured per form.
454
455
456
        if ($formName !== false) {
            Session::checkSessionExpired($this->formSpec[F_SESSION_TIMEOUT_SECONDS]);
        }
457

458
459
460
461
462
463
464
        // Fill STORE_FORM: might need Form.fillStoreVar={{!SELECT ...}}) to provide STORE_VAR - therefore the FORM-definition should already been processed. #8058
        switch ($formMode) {
            case FORM_UPDATE:
            case FORM_SAVE:
            case FORM_REST:
                $fillStoreForm = new FillStoreForm();
                $fillStoreForm->process($formMode);
465
466
467
468
469
470

                // STORE_TYPO3 has been filled: fire fillStoreVar again.
                if (!empty($this->formSpec[FE_FILL_STORE_VAR])) {
                    $this->fillStoreVar($this->formSpec[FE_FILL_STORE_VAR]);
                }

471
472
473
                break;
        }

474
        if ($formName !== false) {
Carsten  Rose's avatar
Carsten Rose committed
475
            // Validate (only if there is a 'real' form, not a FORM_DELETE with only a table name).
Carsten  Rose's avatar
Carsten Rose committed
476
477
            // Attention: $formModeNew will be set
            $sipFound = $this->validateForm($foundInStore, $formMode, $formModeNew);
478
479

        } else {
480
            // FORM_DELETE without a form definition: Fake the form with only a tableName.
481
482
            $table = $this->store->getVar(SIP_TABLE, STORE_SIP);
            if ($table === false) {
Marc Egger's avatar
Marc Egger committed
483
                throw new \UserFormException("No 'form' and no 'table' definition found.", ERROR_MISSING_VALUE);
484
            }
485

486
487
488
            $sipFound = true;
            $this->formSpec[F_NAME] = '';
            $this->formSpec[F_TABLE_NAME] = $table;
489
490
            $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS] = 1; // just indicate a timeout, the exact timeout is stored in the dirty record.
            $this->formSpec[F_DIRTY_MODE] = DIRTY_MODE_EXCLUSIVE; // just set a mode,, the exact mode is stored in the dirty record.
491
            $this->formSpec[F_PRIMARY_KEY] = F_PRIMARY_KEY_DEFAULT;
492
493
494
495
496
497
498
499
500
501

            $tmpDbIndexData = $this->store->getVar(PARAM_DB_INDEX_DATA, STORE_SIP);
            if (!empty($tmpDbIndexData)) {
                $this->formSpec[F_DB_INDEX] = $tmpDbIndexData;
                if ($tmpDbIndexData != $this->dbIndexData) {
                    if (!isset($this->dbArray[$tmpDbIndexData])) {
                        $this->dbArray[$tmpDbIndexData] = new Database($tmpDbIndexData);
                    }
                }
            }
502
503
        }

504
        // FormAsFile: Get the new and the old form file name and make sure both files are writable (before DB changes are made)
505
        // Note: This can't be done earlier because $formModeNew might be changed in the lines above.
506
        $formNameDB = FormAsFile::formNameFromFormRelatedRecord($recordId, $this->formSpec[F_TABLE_NAME], $this->dbArray[$this->dbIndexQfq]);
507
508
509
        switch ($this->formSpec[F_TABLE_NAME]) {
            case TABLE_NAME_FORM: // cases covered: new form, existing form, existing form but form name changed
                $formFileName = $this->store->getVar(F_NAME, STORE_FORM, SANITIZE_ALLOW_ALNUMX);
510
511
512
                $formFileName = $formFileName === false ? $formNameDB : $formFileName;
                if ($formNameDB !== null && $formFileName !== $formNameDB && $formModeNew === FORM_SAVE) {
                    $formFileNameDelete = $formNameDB;
513
                    FormAsFile::enforceFormFileWritable($formFileNameDelete, $this->dbArray[$this->dbIndexQfq]); // file will be deleted after DB changes
514
515
516
517
                }
                break;
            case TABLE_NAME_FORM_ELEMENT: // cases covered: new formElement, existing formElement
                $formId = $this->store->getVar(FE_FORM_ID, STORE_FORM);
518
                $formFileName = $formId !== false ? FormAsFile::formNameFromFormRelatedRecord($formId, TABLE_NAME_FORM, $this->dbArray[$this->dbIndexQfq]) : $formNameDB;
519
520
                break;
            default:
521
                $formFileName = $formNameDB;
522
        }
523
        if ($formFileName !== null && in_array($formModeNew, [FORM_SAVE, FORM_DRAG_AND_DROP, FORM_DELETE])) {
524
            FormAsFile::enforceFormFileWritable($formFileName, $this->dbArray[$this->dbIndexQfq]);
525
526
        }

527
528
        // For 'new' record always create a new Browser TAB-uniq (for this current form, nowhere else used) SIP.
        // With such a Browser TAB-uniq SIP, multiple Browser TABs and following repeated NEWs are easily implemented.
529
530
531
532
        if ($formMode != FORM_REST) {
            if (!$sipFound || ($formMode == FORM_LOAD && $recordId === 0)) {
                $this->store->createSipAfterFormLoad($formName);
            }
533
        }
534

535
        // Fill STORE_BEFORE
536
        if ($formName !== false && $this->store->getVar($this->formSpec[F_PRIMARY_KEY], STORE_BEFORE) === false) {
537
            $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId,
538
                $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY], STORE_BEFORE);
539
540
        }

541
        // Check (and release) dirtyRecord.
Carsten  Rose's avatar
Carsten Rose committed
542
        if ($formModeNew === FORM_DELETE || $formModeNew === FORM_SAVE) {
543
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
Carsten  Rose's avatar
Carsten Rose committed
544

Carsten  Rose's avatar
Carsten Rose committed
545
            $answer = $dirty->checkDirtyAndRelease($formModeNew, $this->formSpec[F_RECORD_LOCK_TIMEOUT_SECONDS],
546
                $this->formSpec[F_DIRTY_MODE], $this->formSpec[F_TABLE_NAME], $this->formSpec[F_PRIMARY_KEY], $recordId, true);
547

548
            // In case of a conflict, return immediately
Carsten  Rose's avatar
Carsten Rose committed
549
550
            if ($answer[API_STATUS] != API_ANSWER_STATUS_SUCCESS) {
                $answer[API_STATUS] = API_ANSWER_STATUS_ERROR;
551

552
553
                return $answer;
            }
Carsten  Rose's avatar
Carsten Rose committed
554
555
        }

556
        // FORM_LOAD: if there is a foreign exclusive record lock - show form in F_MODE_READONLY mode.
Carsten  Rose's avatar
Carsten Rose committed
557
        if ($formModeNew === FORM_LOAD) {
558
            $dirty = new Dirty(false, $this->dbIndexData, $this->dbIndexQfq);
559
560
561
            $recordDirty = array();
            $rcLockFound = $dirty->getCheckDirty($this->formSpec[F_TABLE_NAME], $recordId, $recordDirty, $msg);
            if (($rcLockFound == LOCK_FOUND_CONFLICT || $rcLockFound == LOCK_FOUND_OWNER) && $recordDirty[F_DIRTY_MODE] == DIRTY_MODE_EXCLUSIVE) {
562
                $this->formSpec[F_MODE_GLOBAL] = F_MODE_READONLY;
563
564
565
            }
        }

Carsten  Rose's avatar
Carsten Rose committed
566
        switch ($formModeNew) {
567
568
569
570
571
572
573
574
575
            case FORM_DELETE:
                $build = new Delete($this->dbIndexData);
                break;
            case FORM_REST:
                break;
            case FORM_LOAD:
            case FORM_SAVE:
            case FORM_UPDATE:
            case FORM_DRAG_AND_DROP:
576

577
578
579
                $tableDefinition = $this->dbArray[$this->dbIndexData]->getTableDefinition($this->formSpec[F_TABLE_NAME]);
                $this->store->fillStoreTableDefaultColumnType($tableDefinition);

580
581
                // Check if the defined column primary key exist.
                if ($this->store::getVar($this->formSpec[F_PRIMARY_KEY], STORE_TABLE_COLUMN_TYPES) === false) {
582
                    throw new \UserFormException("Primary Key '" . $this->formSpec[F_PRIMARY_KEY] . "' not found in table " . $this->formSpec[F_TABLE_NAME], ERROR_INVALID_OR_MISSING_PARAMETER);
583
584
                }

585
586
587
588
589
590
591
592
593
594
595
                switch ($this->formSpec['render']) {
                    case 'plain':
                        $build = new BuildFormPlain($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
                        break;
                    case 'table':
                        $build = new BuildFormTable($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
                        break;
                    case 'bootstrap':
                        $build = new BuildFormBootstrap($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->dbArray);
                        break;
                    default:
Marc Egger's avatar
Marc Egger committed
596
                        throw new \CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
597
598
599
                }
                break;
            default:
Marc Egger's avatar
Marc Egger committed
600
                throw new \CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
601
602
        }

603
        $formAction = new FormAction($this->formSpec, $this->dbArray[$this->dbIndexData], $this->phpUnit);
Carsten  Rose's avatar
Carsten Rose committed
604
        switch ($formModeNew) {
605
            case FORM_LOAD:
606
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
607
608

                // Build FORM
Carsten  Rose's avatar
Carsten Rose committed
609
                $data = $build->process($formModeNew);
610

611
612
613
614
                $tmpClass = is_numeric($this->formSpec[F_BS_COLUMNS]) ? ('col-md-' . $this->formSpec[F_BS_COLUMNS]) : $this->formSpec[F_BS_COLUMNS];
//                $data = Support::wrapTag("<div class='" . 'col-md-' . $this->formSpec[F_BS_COLUMNS] . "'>", $data);
                $data = Support::wrapTag('<div class="' . $tmpClass . '">', $data);
                $data = Support::wrapTag('<div class="row">', $data);
615
616
617
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
                break;

Carsten  Rose's avatar
Carsten Rose committed
618
            case FORM_UPDATE:
619
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
620
                // data['form-update']=....
Carsten  Rose's avatar
Carsten Rose committed
621
                $data = $build->process($formModeNew);
622
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
623
                break;
Carsten  Rose's avatar
Carsten Rose committed
624

625
626
627
            case FORM_DELETE:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_DELETE);

628
                $build->process($this->formSpec[F_TABLE_NAME], $recordId, $this->formSpec[F_PRIMARY_KEY]);
629
630
631
632

                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_DELETE);
                break;

633
            case FORM_SAVE:
634
635
                $this->logFormSubmitRequest();

Carsten  Rose's avatar
Carsten Rose committed
636
                $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_TYPO3);
637

638
                // Action: Before
639
640
                $feTypeList = FE_TYPE_BEFORE_SAVE . ',' . ($recordId == 0 ? FE_TYPE_BEFORE_INSERT : FE_TYPE_BEFORE_UPDATE);
                $formAction->elements($recordId, $this->feSpecAction, $feTypeList);
641

642
                // If an old record exist: load it. Necessary to delete uploaded files which should be overwritten.
643
                $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $recordId,
644
                    $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]);
645

646
647
                $this->ifPillIsHiddenSetChildFeToHidden();

648
                // SAVE
649
                $save = new Save($this->formSpec, $this->feSpecAction, $this->feSpecNative, $this->feSpecNativeRaw);
650
651

                $save->processAllImageCutFE();
652
                $save->checkRequiredHidden();
653

654
655
                $rc = $save->process();

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
656
657
                $save->processAllUploads($rc);

658
659
                // Action: After*, Sendmail
                $feTypeList = FE_TYPE_SENDMAIL . ',' . FE_TYPE_AFTER_SAVE . ',' . ($recordId == 0 ? FE_TYPE_AFTER_INSERT : FE_TYPE_AFTER_UPDATE);
660
                $status = $formAction->elements($rc, $this->feSpecAction, $feTypeList);
661
                if ($status != ACTION_ELEMENT_NO_CHANGE) {
662
                    // Reload fresh saved record and fill STORE_RECORD with it.
663
                    $this->store->fillStoreWithRecord($this->formSpec[F_TABLE_NAME], $rc, $this->dbArray[$this->dbIndexData], $this->formSpec[F_PRIMARY_KEY]);
664
                }
665

Carsten  Rose's avatar
Carsten Rose committed
666
667
668
                // Action: Paste
                $this->pasteClipboard($this->formSpec[F_ID], $formAction);

Carsten  Rose's avatar
Carsten Rose committed
669
670
                if ($formMode == FORM_REST) {
                    $data = ['id' => $rc];
671
                    $flagApiStructureReGroup = false;
Carsten  Rose's avatar
Carsten Rose committed
672
673
                    break;
                }
674

675
                $this->setForwardModePage();
676
677

                // Logic: If a) r=0 and
678
                //           b) final: (forwardMode=='auto' and User presses only 'save' (not 'save & close')) OR (forwardMode=='no')
679
                // then the client should reload the current page with the newly created record. A new SIP is necessary!
680
                $getJson = true;
681
682
683
684
                if (0 == $this->store->getVar(SIP_RECORD_ID, STORE_SIP)
                    && (($this->formSpec[F_FORWARD_MODE] == F_FORWARD_MODE_AUTO
                            && API_SUBMIT_REASON_SAVE == $this->store->getVar(API_SUBMIT_REASON, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALNUMX)
                        ) || $this->formSpec[F_FORWARD_MODE] == F_FORWARD_MODE_NO)) {
685
                    $this->formSpec = $this->buildNSetReloadUrl($this->formSpec, $rc);
686
                    $getJson = false;
687
                }
688

689
                if ($getJson) {
690

691
                    // Values of FormElements might be changed during 'afterSave': rebuild the form to load the new values. Especially for non primary template groups.
692
693
694
695
                    $feSpecNative = $this->getNativeFormElements(SQL_FORM_ELEMENT_NATIVE_TG_COUNT, [$this->formSpec[F_ID]], $this->formSpec);
                    $parameterLanguageFieldName = $this->store->getVar(SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME, STORE_SYSTEM);
                    $feSpecNative = HelperFormElement::setLanguage($feSpecNative, $parameterLanguageFieldName);
                    $this->feSpecNative = HelperFormElement::setFeContainerFormElementId($feSpecNative, $this->formSpec[F_ID], $recordId);
696

Carsten  Rose's avatar
Carsten Rose committed
697
                    $data = $build->process($formModeNew, false, $this->feSpecNative);
698
                }
699
                break;
700

701
702
703
            case FORM_DRAG_AND_DROP:
                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);

704
                $dragAndDrop = new DragAndDrop($this->formSpec);
705
                $data = $dragAndDrop->process();
706
                $flagApiStructureReGroup = false;
707
708

                $formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
709
                break;
Carsten  Rose's avatar
Carsten Rose committed
710

711
            case FORM_REST:
712
                $flagApiStructureReGroup = false;
713
                $data = $this->doRestGet();
714
715
                break;

716
            default:
Marc Egger's avatar
Marc Egger committed
717
                throw new \CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
718
719
        }

720
        if ($flagApiStructureReGroup && is_array($data)) {
721
            // $data['element-update']=...
722
723
            $data = $this->groupElementUpdateEntries($data);
        }
Carsten  Rose's avatar
Carsten Rose committed
724

725
        // export Form to file, if loaded record is a Form/FormElement
726
        if ($formFileName !== null) {
727
728
729
            switch ($formModeNew) {
                case FORM_SAVE:
                case FORM_DRAG_AND_DROP:
730
                    FormAsFile::exportForm($formFileName, $this->dbArray[$this->dbIndexQfq]);
731
732
733
                    break;
                case FORM_DELETE:
                    if (TABLE_NAME_FORM_ELEMENT === $this->formSpec[F_TABLE_NAME]) {
734
                        FormAsFile::exportForm($formFileName, $this->dbArray[$this->dbIndexQfq]);
735
                    } else {
736
                        FormAsFile::deleteFormFile($formFileName, $this->dbArray[$this->dbIndexQfq]);
737
                    }
738
                    break;
739
            }
740
741
        }

742
743
        // delete old form file if form name was changed
        if (isset($formFileNameDelete)) {
744
            FormAsFile::deleteFormFile($formFileNameDelete, $this->dbArray[$this->dbIndexQfq]);
745
746
        }

Carsten  Rose's avatar
Carsten Rose committed
747
        return $data;
748
749
    }

750
751
    /**
     * @return array
Marc Egger's avatar
Marc Egger committed
752
753
754
755
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
756
     */
757
    private function doRestGet() {
758

Carsten  Rose's avatar
Carsten Rose committed
759
        $this->nameGenericRestParam();
760
761
762
763

        $r = $this->store::getVar(TYPO3_RECORD_ID, STORE_TYPO3);
        $key = empty($r) ? F_REST_SQL_LIST : F_REST_SQL_DATA;

764
        if (!isset($this->formSpec[$key])) {
Marc Egger's avatar
Marc Egger committed
765
            throw new \UserFormException("Missing Parameter '$key'", ERROR_INVALID_VALUE);
766
767
768
769
770
771
        }

        return $this->evaluate->parse($this->formSpec[$key]);

    }

Carsten  Rose's avatar
Carsten Rose committed
772
773
774
775
776
    /**
     * Checks if $serverToken matches HTTP_HEADER_AUTHORIZATION,
     * If not: throw an exception.
     *
     * @param string|array $serverToken
Marc Egger's avatar
Marc Egger committed
777
778
     * @throws \CodeException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
779
780
781
782
783
784
785
786
     */
    private function restCheckAuthToken($serverToken) {

        // No serverToken: no check necessary
        if ($serverToken === '') {
            return;
        }

787
788
        $clientToken = $this->store::getVar(HTTP_HEADER_AUTHORIZATION, STORE_CLIENT, SANITIZE_ALLOW_ALL);
        if ($serverToken === $clientToken) {
Carsten  Rose's avatar
Carsten Rose committed
789
790
791
792
793
794
795
            return;
        }

        // Delay before answering.
        $seconds = $this->store::getVar(SYSTEM_SECURITY_FAILED_AUTH_DELAY, STORE_SYSTEM);
        sleep($seconds);

796
        if ($clientToken == false) {
Marc Egger's avatar
Marc Egger committed
797
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Missing authorization token',
Marc Egger's avatar
Marc Egger committed
798
                ERROR_MESSAGE_TO_DEVELOPER => "Missing HTTP Header: " . HTTP_HEADER_AUTHORIZATION,
799
                ERROR_MESSAGE_HTTP_STATUS => HTTP_401_UNAUTHORIZED
800
801
802
            ]), ERROR_REST_AUTHORIZATION);
        }

Marc Egger's avatar
Marc Egger committed
803
        throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'Authorization token not accepted',
Marc Egger's avatar
Marc Egger committed
804
            ERROR_MESSAGE_TO_DEVELOPER => "Missing HTTP Header: " . HTTP_HEADER_AUTHORIZATION,
805
            ERROR_MESSAGE_HTTP_STATUS => HTTP_401_UNAUTHORIZED
806
        ]), ERROR_REST_AUTHORIZATION);
Carsten  Rose's avatar
Carsten Rose committed
807
808
    }

809
810
811
    /**
     * STORE_CLIENT: copy parameter _id1,_id2,...,_idN to named variables, specified via $this->formSpec[F_REST_PARAM] (CSV list)
     *
Marc Egger's avatar
Marc Egger committed
812
813
     * @throws \CodeException
     * @throws \UserFormException
814
     */
Carsten  Rose's avatar
Carsten Rose committed
815
    private function nameGenericRestParam() {
816
817
818
819
820
821
822
823

        $paramNames = explode(',', $this->formSpec[F_REST_PARAM] ?? '');

        $ii = 1;
        foreach ($paramNames as $key) {
            switch ($key) {
                case CLIENT_FORM:
                case CLIENT_RECORD_ID:
Marc Egger's avatar
Marc Egger committed
824
                    throw new \UserFormException("Name '$key' is forbidden in " . F_REST_PARAM, ERROR_INVALID_VALUE);
825
826
827
828
829
830
831
832
833
834
                    break;
                default:
                    break;
            }
            $val = $this->store::getVar(CLIENT_REST_ID . $ii, STORE_CLIENT);
            $this->store::setVar($key, $val, STORE_CLIENT);
            $ii++;
        }
    }

835
836
837
    /**
     * Copies state 'hidden' from a FE pill to all FE child elements of that pill.
     *
Marc Egger's avatar
Marc Egger committed
838
839
840
841
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
842
843
844
845
846
847
848
849
     */
    private function ifPillIsHiddenSetChildFeToHidden() {

        $feFilter = OnArray::filter($this->feSpecNative, FE_TYPE, FE_TYPE_PILL);

        if (!empty($feFilter)) {
            foreach ($feFilter AS $feParent) {

850
                if ($feParent[FE_MODE_SQL]) {
851
                    $mode = $this->evaluate->parse($feParent[FE_MODE_SQL]);
852
853
                    if ($mode != '') {
                        $feParent[FE_MODE] = $mode;
854
855
856
857
858
859
860
861
                    }
                }

                if ($feParent[FE_MODE] == FE_MODE_HIDDEN) {
                    $feChild = OnArray::filter($this->feSpecNative, FE_ID_CONTAINER, $feParent[FE_ID]);
                    foreach ($feChild AS $fe) {

                        # Search for origin
862
863
                        foreach ($this->feSpecNative as $key => $value) {
                            if ($value[FE_ID] == $fe[FE_ID]) {
864
865
866
867
868
869
870
871
872
873
                                $this->feSpecNative[$key][FE_MODE] = FE_MODE_HIDDEN;
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

874
    /**
Marc Egger's avatar
Marc Egger committed
875
876
877
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
878
879
880
881
882
883
884
885
886
887
888
     */
    private function logFormSubmitRequest() {
        $formSubmitLogMode = $this->formSpec[F_FORM_SUBMIT_LOG_MODE] ??
            $this->store->getVar(SYSTEM_FORM_SUBMIT_LOG_MODE, STORE_SYSTEM, SANITIZE_ALLOW_ALNUMX);
        if ($formSubmitLogMode === FORM_SUBMIT_LOG_MODE_NONE) {
            return;
        }

        $formData = $_POST;
        unset($formData[CLIENT_SIP]);
        $formData = json_encode($formData, JSON_UNESCAPED_UNICODE);
Carsten  Rose's avatar
Carsten Rose committed
889
890
        $clientIp = $_SERVER[CLIENT_REMOTE_ADDRESS] ?? '';
        $userAgent = $_SERVER[CLIENT_HTTP_USER_AGENT] ?? '';
891
892
893
894
895
896
897
        $sipData = json_encode($this->store->getStore(STORE_SIP), JSON_UNESCAPED_UNICODE);
        $formId = $this->formSpec[F_ID];
        $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP);
        $feUser = $this->store->getVar(TYPO3_FE_USER, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX);
        $pageId = $this->store->getVar(TYPO3_PAGE_ID, STORE_TYPO3, SANITIZE_ALLOW_ALNUMX);
        $sessionId = session_id();

898
        $sql = "INSERT INTO `FormSubmitLog` (`formData`, `sipData`, `clientIp`, `feUser`, `userAgent`, `formId`, `recordId`, `pageId`, `sessionId`, `created`)" .
899
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())";
900
901
902
903
        $params = [$formData, $sipData, $clientIp, $feUser, $userAgent, $formId, $recordId, $pageId, $sessionId];
        $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $params);
    }

904
905
906
907
908
909
910
911
912
913
914

    /**
     * Check if forwardMode='url...'.
     * yes: process 'forwardPage' and fill $this->formSpec[F_FORWARD_MODE] and $this->formSpec[F_FORWARD_PAGE]
     * no: do nothing
     *
     * '$this->formSpec[F_FORWARD_PAGE]' might give a new forwardMode. If so, set $this->formSpec[F_FORWARD_MODE] to
     * it.
     *
     * '$this->formSpec[F_FORWARD_PAGE]':
     * a) url     http://www.nzz.ch/index.html?a=123#bottom, website.html?a=123#bottom,
915
     *            ?[id=]<T3 Alias pageId>&a=123#bottom, ?id=<T3 pageId>&a=123#bottom
916
917
918
     * b) mode      no|client|url|...
     * c) mode|url  combination of above
     *
Marc Egger's avatar
Marc Egger committed
919
920
921
922
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
923
924
925
     */
    private function setForwardModePage() {