FormAction.php 28 KB
Newer Older
1
2
3
4
5
6
7
8
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 5/29/16
 * Time: 5:24 PM
 */

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

Marc Egger's avatar
Marc Egger committed
11
use IMATHUZH\Qfq\Core\Database\Database;
12
use IMATHUZH\Qfq\Core\Evaluate;
Marc Egger's avatar
Marc Egger committed
13
use IMATHUZH\Qfq\Core\Helper\HelperFile;
14
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
Marc Egger's avatar
Marc Egger committed
15
16
17
18
19
use IMATHUZH\Qfq\Core\Helper\Ldap;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Report\SendMail;
20
use IMATHUZH\Qfq\Core\Store\Store;
21

22

23
24
25
26
/**
 * Class formAction
 * @package qfq
 */
27
class FormAction {
28
29

//    private $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form
30
31
32
33
    /**
     * @var Evaluate instantiated class
     */
    protected $evaluate = null;  // copy of the loaded form
Carsten  Rose's avatar
Carsten Rose committed
34

35
    private $formSpec = array();
36
    private $primaryTableName = '';
Carsten  Rose's avatar
Carsten Rose committed
37

38
39
40
41
    /**
     * @var Database
     */
    private $db = null;
Carsten  Rose's avatar
Carsten Rose committed
42

43
44
45
46
47
48
49
50
51
    /**
     * @var Store
     */
    private $store = null;

    /**
     * @param array $formSpec
     * @param Database $db
     * @param bool|false $phpUnit
Marc Egger's avatar
Marc Egger committed
52
53
54
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
55
56
     */
    public function __construct(array $formSpec, Database $db, $phpUnit = false) {
57
58

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
59
        $this->formSpec = $formSpec;
60
        $this->primaryTableName = Support::setIfNotSet($formSpec, F_TABLE_NAME);
61
62
63
64
65
66
67
68
69

        $this->db = $db;

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

        $this->evaluate = new Evaluate($this->store, $this->db);

    }

70
    /**
71
72
     * Parse $fillStoreVar and if something is given, add it to STORE_VAR.
     *
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
     * @param string $fillStoreVar
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function feFillStoreVar($fillStoreVar) {

        if ($fillStoreVar != '') {

            $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_FILL_STORE_VAR, STORE_SYSTEM); // debug
            $rows = $this->evaluate->parse($fillStoreVar, ROW_EXPECT_0_1);

            if (is_array($rows)) {
                $this->store->appendToStore($rows, STORE_VAR);
            } else {
                if (!empty($rows)) {
                    throw new \UserFormException(json_encode(
                        [ERROR_MESSAGE_TO_USER => "Invalid statement for 'fillStoreVar'.",
                            ERROR_MESSAGE_TO_DEVELOPER => $fillStoreVar]), ERROR_INVALID_OR_MISSING_PARAMETER);
                }
            }
        }

    }

99
100
    /**
     * @param integer $recordId
Carsten  Rose's avatar
Carsten Rose committed
101
102
     * @param array $feSpecAction
     * @param string $feTypeList
103
104
105
     *         On FormLoad: FE_TYPE_BEFORE_LOAD, FE_TYPE_AFTER_LOAD
     *         Before Save: FE_TYPE_BEFORE_SAVE, FE_TYPE_BEFORE_INSERT, FE_TYPE_BEFORE_UPDATE, FE_TYPE_BEFORE_DELETE
     *         After Save: FE_TYPE_AFTER_SAVE, FE_TYPE_AFTER_INSERT, FE_TYPE_AFTER_UPDATE, FE_TYPE_AFTER_DELETE
Carsten  Rose's avatar
Carsten Rose committed
106
     *
107
108
109
     * @return int: ACTION_ELEMENT_MODIFIED if there are potential changes on the DB like fired SQL statements,
     *              ACTION_ELEMENT_NO_CHANGE if nothing happened
     *              ACTION_ELEMENT_DELETED:  if a record has been deleted (only in recursive calls, not the initial one)
Marc Egger's avatar
Marc Egger committed
110
111
112
113
114
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
115
116
117
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
118
     */
119
    public function elements($recordId, array $feSpecAction, $feTypeList) {
120

121
        $rc = ACTION_ELEMENT_NO_CHANGE;
122

123
124
125
        // Iterate over all Action FormElements
        foreach ($feSpecAction as $fe) {

126
            // Preparation for Log, Debug
127
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); // debug
128
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID] ?? '', STORE_SYSTEM); // debug
129

130
            $fe = HelperFormElement::initActionFormElement($fe);
131

132
            // Only process FE elements of types listed in $feTypeList. Skip all other
133
134
135
136
            if (false === Support::findInSet($fe[FE_TYPE], $feTypeList)) {
                continue;
            }

137
138
            $this->feFillStoreVar($fe[FE_FILL_STORE_VAR] ?? '');
            $fe[FE_FILL_STORE_VAR] = ''; // do not process the same later on.
139

140
141
            // Process templateGroup action elements
            if (isset($fe[FE_ID_CONTAINER]) && $fe[FE_ID_CONTAINER] > 0) {
142
                // Get native 'templateGroup'-FE - to retrieve MAX_LENGTH
143
                $templateGroup = $this->db->sql(SQL_FORM_ELEMENT_TEMPLATE_GROUP_FE_ID, ROW_EXPECT_1, [$fe[FE_ID_CONTAINER]],
144
                    "Action FormElements should not be assigned to a container (exception: templateGroup). FormElement.id=" . $fe[FE_ID] . ", feIdContainer=" . $fe[FE_ID_CONTAINER] . ' is not a templateGroup');
145
146
147
148

                if (isset($templateGroup[FE_TYPE]) && $templateGroup[FE_TYPE] == FE_TYPE_TEMPLATE_GROUP) {
                    $maxCopies = HelperFormElement::tgGetMaxLength($templateGroup[FE_MAX_LENGTH]);
                    $fe[FE_ID_CONTAINER] = 0; // Flag to make the nested TG unnested and therefore the SQLs are fired.
149
150
//                    for ($ii = $maxCopies; $ii > 0; $ii--) { // Iterate backwards: deleting records starts at the end and doesn't affect remaining counting
                    $correctDeleteIndex = 0;
151
                    for ($ii = 1; $ii <= $maxCopies; $ii++) {
152
153
154
155
156
157
158
159
160
161
162
163
164
                        $feNew = OnArray::arrayValueReplace($fe, FE_TEMPLATE_GROUP_NAME_PATTERN, $ii - $correctDeleteIndex);
                        $feNew = OnArray::arrayValueReplace($feNew, FE_TEMPLATE_GROUP_NAME_PATTERN_0, $ii - 1 - $correctDeleteIndex);
                        switch ($this->elements($recordId, [$feNew], $feTypeList)) {
                            case ACTION_ELEMENT_MODIFIED:
                                $rc = ACTION_ELEMENT_MODIFIED;
                                break;
                            case ACTION_ELEMENT_DELETED:
                                $rc = ACTION_ELEMENT_MODIFIED;
                                $correctDeleteIndex++;
                                break;
                            case ACTION_ELEMENT_NO_CHANGE:
                            default:
                                break;
165
166
                        }
                    }
167
                    continue; // skip to next FormElement
168
169
                }
            }
170

171
172
173
174
175
176
177
            switch ($fe[FE_TYPE]) {
                case FE_TYPE_BEFORE_LOAD:
                case FE_TYPE_AFTER_LOAD:
                case FE_TYPE_AFTER_DELETE:  # Main record is already deleted. Do not try to load it again.
                    break;
                default:
                    // Always work on recent data: previous actions might have modified the data.
178
                    $this->store->fillStoreWithRecord($this->primaryTableName, $recordId, $this->db, $this->formSpec[F_PRIMARY_KEY] ?? '');
179
180
            }

181
182
183
184
            if (!$this->checkRequiredList($fe)) {
                continue;
            }

185
186
187
188
189
            if (isset($fe[FE_FILL_STORE_LDAP])) {
                $keyNames = [F_LDAP_SERVER, F_LDAP_BASE_DN, F_LDAP_ATTRIBUTES, F_LDAP_SEARCH, F_LDAP_TIME_LIMIT];
                $fe = OnArray::copyArrayItemsIfNotAlreadyExist($this->formSpec, $fe, $keyNames);

                // Extract necessary elements
190
                $config = OnArray::getArrayItems($fe, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES, FE_LDAP_USE_BIND_CREDENTIALS]);
191
192
                $config = $this->evaluate->parseArray($config);

193
                if ($fe[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
194
195
196
197
                    $config[SYSTEM_LDAP_1_RDN] = $this->store->getVar(SYSTEM_LDAP_1_RDN, STORE_SYSTEM);
                    $config[SYSTEM_LDAP_1_PASSWORD] = $this->store->getVar(SYSTEM_LDAP_1_PASSWORD, STORE_SYSTEM);
                }

198
199
200
201
202
                $ldap = new Ldap();
                $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
                $this->store->setStore($arr, STORE_LDAP, true);
            }

203
            $this->sqlValidate($fe);
204

205
            if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) {
206
                $this->doSendMail($fe);
207
            }
208

209
210
211
212
213
214
215
216
217
            $rcTmp = $this->doSqlBeforeSlaveAfter($fe, $recordId, true);
            switch ($rcTmp) {
                case ACTION_ELEMENT_MODIFIED:
                case ACTION_ELEMENT_DELETED:
                    $rc = $rcTmp;
                    break;
                default:
                    break;
            }
218
        }
219

220
        return $rc;
221
222
    }

223
    /**
224
     * Process all FormElements given in the `requiredList` identified by their name.
225
226
227
228
     * If none is empty in STORE_FORM return true, else false.
     * If none FormElement is specified, return true.
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
229
     *
230
     * @return bool  true if none FE is specified or all specified are non empty.
Marc Egger's avatar
Marc Egger committed
231
232
     * @throws \CodeException
     * @throws \UserFormException
233
234
     */
    private function checkRequiredList(array $fe) {
235

236
237
        if (!isset($fe[FE_REQUIRED_LIST]) || $fe[FE_REQUIRED_LIST] === '') {
            return true;
238
239
        }

240
241
242
243
        $arr = explode(',', $fe[FE_REQUIRED_LIST]);
        foreach ($arr as $key) {

            $key = trim($key);
244
            $val = $this->store->getVar($key, STORE_FORM, SANITIZE_ALLOW_ALL);
245
246
247
248

            if ($val === false || $val === '' || $val === '0') {
                return false;
            }
249
250
        }

251
        return true;
252
253
    }

254
255
    /**
     * @param array $feSpecAction
Marc Egger's avatar
Marc Egger committed
256
257
258
259
260
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
261
262
263
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
264
     */
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
    private function doSendMail(array $feSpecAction) {

        $args = array();

        $args[] = SENDMAIL_TOKEN_RECEIVER . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_TO]);
        $args[] = SENDMAIL_TOKEN_SENDER . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FROM]);
        $args[] = SENDMAIL_TOKEN_SUBJECT . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_SUBJECT]);
        $args[] = SENDMAIL_TOKEN_BODY . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_VALUE]);
        $args[] = SENDMAIL_TOKEN_REPLY_TO . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_REPLY_TO]);
        $autoSubmit = ($this->evaluate->parse($feSpecAction[FE_SENDMAIL_FLAG_AUTO_SUBMIT]) === 'off') ? 'off' : 'on';
        $args[] = SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT . PARAM_TOKEN_DELIMITER . $autoSubmit;
        $args[] = SENDMAIL_TOKEN_GR_ID . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_GR_ID]);
        $args[] = SENDMAIL_TOKEN_X_ID . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID]);
        $args[] = SENDMAIL_TOKEN_RECEIVER_CC . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_CC]);
        $args[] = SENDMAIL_TOKEN_RECEIVER_BCC . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BCC]);
        $args[] = SENDMAIL_TOKEN_SRC . PARAM_TOKEN_DELIMITER . "FormId: " . $feSpecAction[FE_FORM_ID] . ", FormElementId: " . $feSpecAction['id'];
        $args[] = SENDMAIL_TOKEN_X_ID2 . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID2]);
        $args[] = SENDMAIL_TOKEN_X_ID3 . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID3]);
283
284
285
        $args[] = SENDMAIL_TOKEN_BODY_MODE . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BODY_MODE]);
        $args[] = SENDMAIL_TOKEN_BODY_HTML_ENTITY . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BODY_HTML_ENTITY]);
        $args[] = SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_SUBJECT_HTML_ENTITY]);
286
        $args[] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_ATTACHMENT] ?? '');
287
288

        // Mail: send
289
290
291
        $sendMail = new SendMail();
        $mailConfig = $sendMail->parseStringToArray(implode(PARAM_DELIMITER, $args));
        $sendMail->process($mailConfig);
292
293
    }

294
295
296
297
    /**
     * If there is a query defined in fe.parameter.FE_SQL_VALIDATE: fire them.
     * Count the selected records and compare them with fe.parameter.FE_EXPECT_RECORDS.
     * If match: everything is fine, do nothing.
Marc Egger's avatar
Marc Egger committed
298
     * Else throw \UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL
299
300
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
301
     *
Marc Egger's avatar
Marc Egger committed
302
303
304
305
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
306
     */
307
    private function sqlValidate(array $fe) {
308
309

        // Is there something to check?
310
        if ($fe[FE_SQL_VALIDATE] === '') {
311
312
313
            return;
        }

314
        if ($fe[FE_EXPECT_RECORDS] === '') {
Marc Egger's avatar
Marc Egger committed
315
            throw new \UserFormException("Missing parameter '" . FE_EXPECT_RECORDS . "'", ERROR_MISSING_EXPECT_RECORDS);
316
        }
317
        $expect = $this->evaluate->parse($fe[FE_EXPECT_RECORDS]);
318

319
        if ($fe[FE_MESSAGE_FAIL] === '') {
Marc Egger's avatar
Marc Egger committed
320
            throw new \UserFormException("Missing parameter '" . FE_MESSAGE_FAIL . "'", ERROR_MISSING_MESSAGE_FAIL);
321
322
323
        }

        // Do the check
324
        $result = $this->evaluate->parse($fe[FE_SQL_VALIDATE], ROW_REGULAR);
325
        if (!is_array($result)) {
Marc Egger's avatar
Marc Egger committed
326
            throw new \UserFormException("Expected an array for '" . FE_SQL_VALIDATE . "', got a scalar. Please check for {{!...", ERROR_EXPECTED_ARRAY);
327
328
        }

329
330
331
332
333
334
        // If there is at least one record count given, who matches: return 'check succeeded'
        $countRecordsArr = explode(',', $expect);
        foreach ($countRecordsArr AS $count) {
            if (count($result) == $count) {
                return; // check succesfully passed
            }
335
336
        }

337
        $msg = $this->evaluate->parse($fe[FE_MESSAGE_FAIL]); // Replace possible dynamic parts
338

339
        // Throw user error message
340
        throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => $msg, ERROR_MESSAGE_TO_DEVELOPER => 'validate() failed']), ERROR_REPORT_FAILED_ACTION);
341

342
343
    }

344
    /**
Carsten  Rose's avatar
Carsten Rose committed
345
346
     * Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or
     * `sqlInsert`
347
348
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
349
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
350
     *
351
     * @param bool $flagFeAction indicates of the FE are of type 'native' or 'action'.
352
353
354
     * @return int  ACTION_ELEMENT_MODIFIED if there are potential(!) changes on the DB like INSERT / UPDATE,
     *              ACTION_ELEMENT_NO_CHANGE if nothing happened
     *              ACTION_ELEMENT_DELETED:  if a record has been deleted
Marc Egger's avatar
Marc Egger committed
355
356
357
358
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
359
     */
360
361
    public function doSqlBeforeSlaveAfter(array $fe, $recordId, $flagFeAction) {

362
363
364
365
366
        // Uploads will be processed later.
        if ($fe[FE_TYPE] == FE_TYPE_UPLOAD) {
            return ACTION_ELEMENT_NO_CHANGE;
        }

367
368
        $rcStatus = ACTION_ELEMENT_NO_CHANGE;

369
370
        $this->feFillStoreVar($fe[FE_FILL_STORE_VAR] ?? '');

371
        // slaveId might be used in sqlBefore: get it first.
Carsten  Rose's avatar
Carsten Rose committed
372
373
374
        if (isset($fe[FE_SLAVE_ID])) {
            // Get the slaveId
            $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]);
375

Carsten  Rose's avatar
Carsten Rose committed
376
377
378
379
            if ($flagFeAction && $slaveId === '' && $fe[FE_NAME] !== '') {
                // If the current action element has the same name as a real master record column: take that value as an id.
                $slaveId = $this->store->getVar($fe[FE_NAME], STORE_RECORD);
            }
380

Carsten  Rose's avatar
Carsten Rose committed
381
382
383
            if ($slaveId === '' || $slaveId === false) {
                $slaveId = 0;
            }
384

Carsten  Rose's avatar
Carsten Rose committed
385
386
            // Store the slaveId: it's used and replaced in the update statement.
            $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);
387
388
389
390
391
392
        } else {

            if (false !== ($id = $this->store->getVar(VAR_SLAVE_ID, STORE_VAR))) {
                $slaveId = $id;
                $fe[FE_SLAVE_ID] = $id;
            }
393
        }
394

395
396
397
398
        // If given: fire a sqlBefore query
        $this->evaluate->parse($fe[FE_SQL_BEFORE]);

        if (isset($fe[FE_SLAVE_ID])) {
Carsten  Rose's avatar
Carsten Rose committed
399
400
401
            $doInsert = ($slaveId == 0);
            $doUpdate = ($slaveId != 0);
            $doDelete = ($slaveId != 0) && !empty($fe[FE_SQL_DELETE]);
402

Carsten  Rose's avatar
Carsten Rose committed
403
404
405
406
407
408
            if (!empty($fe[FE_SQL_HONOR_FORM_ELEMENTS])) {
                $filled = $this->checkFormElements($fe[FE_SQL_HONOR_FORM_ELEMENTS]);
                $doInsert = $filled && $doInsert;
                $doUpdate = $filled && $doUpdate;
                $doDelete = !$filled && $doDelete;
            }
409

Carsten  Rose's avatar
Carsten Rose committed
410
411
412
413
414
415
416
            // Fire slave query
            if ($doInsert) {
                $slaveId = $this->evaluate->parse($fe[FE_SQL_INSERT]);
                // Store the slaveId: might be used later
                $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);
                $rcStatus = ACTION_ELEMENT_MODIFIED;
            }
417

Carsten  Rose's avatar
Carsten Rose committed
418
419
420
421
            if ($doUpdate) {
                $this->evaluate->parse($fe[FE_SQL_UPDATE]);
                $rcStatus = ACTION_ELEMENT_MODIFIED;
            }
422

Carsten  Rose's avatar
Carsten Rose committed
423
424
425
426
427
428
            // Fire a delete query
            if ($doDelete) {
                $this->evaluate->parse($fe[FE_SQL_DELETE]);
                $slaveId = 0;
                $rcStatus = ACTION_ELEMENT_DELETED;
            }
429

Carsten  Rose's avatar
Carsten Rose committed
430
431
432
            // Check if there is a column with the same name as the 'action'-FormElement.
            if ($flagFeAction && false !== $this->store->getVar($fe[FE_NAME], STORE_RECORD)) {
                // After an insert or update, propagate the (new) slave id to the master record.
433
                $this->db->sql("UPDATE `" . $this->primaryTableName . "` SET `" . $fe[FE_NAME] . "` = $slaveId WHERE `id` = ? LIMIT 1", ROW_REGULAR, [$recordId]);
Carsten  Rose's avatar
Carsten Rose committed
434
            }
435
436
        }

437
438
439
        // If given: fire a $sqlAfter query
        $this->evaluate->parse($fe[FE_SQL_AFTER]);

440
        return $rcStatus;
441
    }
442

443
444
445
446
447
    /**
     * Iterates over list of FormElement-names and check STORE_FORM if there is a corresponding value. If at least one
     * of the give elements is non empty, return true. If all elements are empty, return false.
     *
     * @param string $listOfFormElementNames E.g.: 'city, street, number'
Carsten  Rose's avatar
Carsten Rose committed
448
449
450
     *
     * @return bool true if at lease one of the named elements is non empty on STORE_FORM (use SANATIZE_ALLOW_ALL to
     *              perform the check)
Marc Egger's avatar
Marc Egger committed
451
452
     * @throws \CodeException
     * @throws \UserFormException
453
454
455
456
457
458
459
460
461
462
     */
    private function checkFormElements($listOfFormElementNames) {
        $arr = explode(',', $listOfFormElementNames);

        foreach ($arr as $key) {
            $value = $this->store->getVar(trim($key), STORE_FORM . STORE_EMPTY, SANITIZE_ALLOW_ALL);
            if ($value != '') {
                return true;
            }
        }
Carsten  Rose's avatar
Carsten Rose committed
463

464
465
        return false;
    }
466

Carsten  Rose's avatar
Carsten Rose committed
467
468
469
470
471
    /**
     * Will be called for each master record (clipboard).
     * Process all FE.type='paste' for the given master record in clipboard.
     * Will store the clipboard in STORE_PARENT.
     *
Carsten  Rose's avatar
Carsten Rose committed
472
473
     * @param array $feSpecAction - all FE.class='action' - just process 'paste'
     * @param string $recordSourceTable - table name from where to copy the source records
Carsten  Rose's avatar
Carsten Rose committed
474
     * @param string $recordDestinationTable - table name where the records will be duplicated to.
Carsten  Rose's avatar
Carsten Rose committed
475
     * @param string $sub - on the highest level an empty string. It's a filter, value comes from
Carsten  Rose's avatar
Carsten Rose committed
476
     *                                       FE.name, to specify sub-sub copy rules.
Carsten  Rose's avatar
Carsten Rose committed
477
     * @param array $clipboard
Carsten  Rose's avatar
Carsten Rose committed
478
     *
Marc Egger's avatar
Marc Egger committed
479
480
481
482
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
     */
    public function doAllFormElementPaste(array $feSpecAction, $recordSourceTable, $recordDestinationTable, $sub, array $clipboard) {

        # process all paste records
        foreach ($feSpecAction as $formElement) {

            // Set the clipboard as the parent record. Update always the latest created Ids
            $this->store->setStore($clipboard, STORE_PARENT_RECORD, true);

            // Only process FE elements of types listed in $feTypeList. Skip all other.
            if (false === Support::findInSet($formElement[FE_TYPE], FE_TYPE_PASTE) || $formElement[FE_LABEL] != $sub) {
                continue;
            }

            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);

            $formElement = HelperFormElement::initActionFormElement($formElement);

            if (!empty($formElement[FE_RECORD_DESTINATION_TABLE])) {
                $recordDestinationTable = $formElement[FE_RECORD_DESTINATION_TABLE];
                $recordSourceTable = (empty($formElement[FE_RECORD_SOURCE_TABLE])) ? $recordDestinationTable : $formElement[FE_RECORD_SOURCE_TABLE];
            }

            $newValues = $this->evaluate->parse($formElement[FE_SQL1]);

            # Dupliziere den Record. RC ist die ID des neu erzeugten Records.
510
            $lastInsertId = $this->prepareDuplicate($feSpecAction, $formElement, $newValues, $recordSourceTable, $recordDestinationTable, $sub, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
511
512
513
514
515
516
517
518
519
520
521
522
523

            # Lege die Record ID im Array ab, damit spaetere 'paste' Records diese entsprechend einsetzen koennen.
            # Nur falls ein Name angegeben ist und dieser !='id' ist.
            if ($formElement[FE_NAME] !== '' && $formElement[FE_NAME] != COLUMN_ID) {
                $clipboard[$formElement[FE_NAME]] = $lastInsertId;
            }
        }
    } # doAllFormElementPaste()


    /**
     *
     *
Carsten  Rose's avatar
Carsten Rose committed
524
     * @param array $feSpecActionAll - all FE.class='action' - just process 'paste'
525
     * @param array $feSpecAction
Carsten  Rose's avatar
Carsten Rose committed
526
     * @param array $updateRecords - array of records: 'id' is the source.id, all other fields will replace
Carsten  Rose's avatar
Carsten Rose committed
527
     *                                       source columns.
Carsten  Rose's avatar
Carsten Rose committed
528
     * @param        $recordSourceTable - table name from where to copy the source records
Carsten  Rose's avatar
Carsten Rose committed
529
     * @param        $recordDestinationTable - table name where the records will be duplicated to.
Carsten  Rose's avatar
Carsten Rose committed
530
     * @param string $sub - on the highest level an empty string. It's a filter, value comes from
Carsten  Rose's avatar
Carsten Rose committed
531
     *                                       FE.name, to specify sub-sub copy rules.
Carsten  Rose's avatar
Carsten Rose committed
532
     * @param array $clipboard -
Carsten  Rose's avatar
Carsten Rose committed
533
     * @return int - lastInsertId
Marc Egger's avatar
Marc Egger committed
534
535
536
537
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
538
     */
539
540
541
    private function prepareDuplicate(array $feSpecActionAll, array $feSpecAction, array $updateRecords, $recordSourceTable, $recordDestinationTable, $sub, array $clipboard) {
        $translateMap = array();
        $field = $feSpecAction[FE_NAME];
Carsten  Rose's avatar
Carsten Rose committed
542
543
544
545
546
547
548
549
550
551
552
553
554

        // Sometimes there is no query at all.
        if (count($updateRecords) == 0) {
            return (0);
        }

        // Iterate (for the given Paste FE) all updateRecords: duplicate each.
        $lastInsertId = 0;
        foreach ($updateRecords as $newColumns) {

            // will be used in sub paste's
//            $clipboard["_src_id"] = $newColumns[COLUMN_ID];

555
            $rowSrc = $this->db->sql("SELECT * FROM `$recordSourceTable` WHERE `id`=?", ROW_EXPECT_1, [$newColumns[COLUMN_ID]]);
Carsten  Rose's avatar
Carsten Rose committed
556
557
558
559
560
561
562
563
564

            $this->checkNCopyFiles($rowSrc, $newColumns);

            foreach ($newColumns as $key => $val) {
                $rowSrc[$key] = $val;
            }

            $lastInsertId = $this->copyRecord($rowSrc, $recordDestinationTable);
            $clipboard[$field] = $lastInsertId;
565
            $translateMap[$newColumns[COLUMN_ID]] = $lastInsertId;
Carsten  Rose's avatar
Carsten Rose committed
566
567
568
569
570
571

            // Set the clipboard as the primary record as long as secondaries are created.
            $this->store->setStore($clipboard, STORE_PARENT_RECORD, true);

            # Do subqueries
            if ($sub == "") {
572
                $this->doAllFormElementPaste($feSpecActionAll, $recordSourceTable, $recordDestinationTable, $field, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
573
574
575
            }
        }

576
577
578
579
580
        // If necessary: correct table self referencing id columns
        if (!empty($feSpecAction[FE_TRANSLATE_ID_COLUMN])) {
            $this->translateId($translateMap, $feSpecAction[FE_TRANSLATE_ID_COLUMN], $recordDestinationTable);
        }

Carsten  Rose's avatar
Carsten Rose committed
581
582
583
584
        return $lastInsertId;

    } // prepareDuplicate()

585
586
587
588
589
590
591
592
593
594
595
596
597
    /**
     * Translate table self referencing columns to the new values.
     * Rerun on all new records. Search and translate old id's (copied) to the new generated id's.
     *
     * Example with FormElement:  id, feIdContainer, type
     *
     *     Original:  [1,2,'input'], [2,3,'templateGroup'], [3,0, 'pill']
     *   Duplicated:  [4,2,'input'], [5,3,'templateGroup'], [6,0, 'pill']
     *  TranslateId:  [4,5,'input'], [5,6,'templateGroup'], [6,0, 'pill']
     *
     * @param array $translateMap array with old id's as keys, and new id's as their value
     * @param string $translateIdColumn column name to update. E.g. FormElement.feIdContainer, Ggroup.grId, ...
     * @param string $tableName
Marc Egger's avatar
Marc Egger committed
598
599
600
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
601
602
603
604
605
     */
    private function translateId(array $translateMap, $translateIdColumn, $tableName) {

        foreach ($translateMap as $oldId => $newId) {

606
            $row = $this->db->sql("SELECT `$translateIdColumn` FROM `$tableName` WHERE `id`=$newId", ROW_EXPECT_1);
607
608
609

            if (!empty($row[$translateIdColumn])) {
                $newNewId = $translateMap[$row[$translateIdColumn]];
610
                $this->db->sql("UPDATE `$tableName` SET `$translateIdColumn`=$newNewId WHERE `id`=$newId LIMIT 1");
611
612
613
614
615
            }

        }
    }

Carsten  Rose's avatar
Carsten Rose committed
616
617
618
    /**
     * @param array $rowSrc
     * @param array $rowDest
Carsten  Rose's avatar
Carsten Rose committed
619
     *
Marc Egger's avatar
Marc Egger committed
620
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
     */
    private function checkNCopyFiles(array $rowSrc, array $rowDest) {

        foreach ($rowSrc as $key => $val) {

            // Skip non 'special file column'.
            if (false === strpos($key, COLUMN_PATH_FILE_NAME)) {
                continue;
            }

            // If a/b) the target is empty, c) src & dest is equal, d) src is not a file: there is nothing to copy.
            if (empty($rowDest[$key]) || ($val === $rowDest[$key]) || !is_file($val)) {
                continue;
            }

Carsten  Rose's avatar
Carsten Rose committed
636
            HelperFile::mkDirParent($rowDest[$key]);
637
            HelperFile::copy($val, $rowDest[$key]);
Carsten  Rose's avatar
Carsten Rose committed
638
639
640
641
642
643
644
645
646
        }
    }

    /**
     * Copy $row to $destable.
     * Copy only values which have a column in $destTable.
     * If there is nothing to copy - Do nothing.
     * Columns with name 'id', 'modified' or 'created' are skipped.
     *
Carsten  Rose's avatar
Carsten Rose committed
647
     * @param array $row
Carsten  Rose's avatar
Carsten Rose committed
648
     * @param string $destTable
Carsten  Rose's avatar
Carsten Rose committed
649
     *
Carsten  Rose's avatar
Carsten Rose committed
650
     * @return int - lastInsertId
Marc Egger's avatar
Marc Egger committed
651
652
653
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
654
655
656
657
658
659
660
661
662
663
     */
    function copyRecord(array $row, $destTable) {
        $keys = array();
        $values = array();
        $placeholder = array();

        $columns = $this->db->sql("SHOW FIELDS FROM " . $destTable);

        // Process all columns of destTable
        foreach ($columns as $col) {
664

Carsten  Rose's avatar
Carsten Rose committed
665
            $key = $col[COLUMN_FIELD];
Carsten  Rose's avatar
Carsten Rose committed
666
            // Only copy columns which exist on source AND destination.
667
668
669
            if (!isset($row[$key])) {
                continue;
            }
Carsten  Rose's avatar
Carsten Rose committed
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
            $val = $row[$key];

            switch ($key) {
                case COLUMN_ID:
                    continue 2;
                case COLUMN_MODIFIED:
                case COLUMN_CREATED:
                    $keys[] = $key;
                    $placeholder[] = 'NOW()';
                    continue 2;
            }

            if (isset($row[$key])) {
                $keys[] = $key;
                $values[] = $val;
                $placeholder[] = '?';
            }
        }

        // If there is nothing to write: return
        if (count($values) == 0) {
            return (0);
        }

694
        $keyString = '`' . implode('`,`', $keys) . '`';
Carsten  Rose's avatar
Carsten Rose committed
695
696
        $valueString = implode(',', $placeholder);

697
        $sql = "INSERT INTO `$destTable` ($keyString) VALUES ($valueString)";
Carsten  Rose's avatar
Carsten Rose committed
698
699
700
701

        return $this->db->sql($sql, ROW_REGULAR, $values);

    } # copyRecord()
702
}