FormAction.php 27.6 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
12
13
14
15
16
17
18
19
20
21
use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
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\Store\Store;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Report\SendMail;
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
 
22

23

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

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

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

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

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

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

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

        $this->db = $db;

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

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

    }

71
72
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
    /**
     * @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);
                }
            }
        }

    }

98
99
    /**
     * @param integer $recordId
Carsten  Rose's avatar
Carsten Rose committed
100
101
     * @param array $feSpecAction
     * @param string $feTypeList
102
103
104
     *         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
105
     *
106
107
108
     * @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
109
110
111
112
113
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
114
115
116
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
117
     */
118
    public function elements($recordId, array $feSpecAction, $feTypeList) {
119

120
        $rc = ACTION_ELEMENT_NO_CHANGE;
121

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

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

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

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

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

139
140
            // Process templateGroup action elements
            if (isset($fe[FE_ID_CONTAINER]) && $fe[FE_ID_CONTAINER] > 0) {
141
                // Get native 'templateGroup'-FE - to retrieve MAX_LENGTH
142
                $templateGroup = $this->db->sql(SQL_FORM_ELEMENT_TEMPLATE_GROUP_FE_ID, ROW_EXPECT_1, [$fe[FE_ID_CONTAINER]],
143
                    "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');
144
145
146
147

                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.
148
149
//                    for ($ii = $maxCopies; $ii > 0; $ii--) { // Iterate backwards: deleting records starts at the end and doesn't affect remaining counting
                    $correctDeleteIndex = 0;
150
                    for ($ii = 1; $ii <= $maxCopies; $ii++) {
151
152
153
154
155
156
157
158
159
160
161
162
163
                        $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;
164
165
                        }
                    }
166
                    continue; // skip to next FormElement
167
168
                }
            }
169

170
171
172
173
174
175
176
            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.
177
                    $this->store->fillStoreWithRecord($this->primaryTableName, $recordId, $this->db, $this->formSpec[F_PRIMARY_KEY]??'');
178
179
            }

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

184
185
186
187
188
            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
189
                $config = OnArray::getArrayItems($fe, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES, FE_LDAP_USE_BIND_CREDENTIALS]);
190
191
                $config = $this->evaluate->parseArray($config);

192
                if ($fe[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
193
194
195
196
                    $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);
                }

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

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

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

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

219
        return $rc;
220
221
    }

222
    /**
223
     * Process all FormElements given in the `requiredList` identified by their name.
224
225
226
227
     * 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
228
     *
229
     * @return bool  true if none FE is specified or all specified are non empty.
Marc Egger's avatar
Marc Egger committed
230
231
     * @throws \CodeException
     * @throws \UserFormException
232
233
     */
    private function checkRequiredList(array $fe) {
234

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

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

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

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

250
        return true;
251
252
    }

253
254
    /**
     * @param array $feSpecAction
Marc Egger's avatar
Marc Egger committed
255
256
257
258
259
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
260
261
262
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
263
     */
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
    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]);
282
283
284
        $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]);
285
        $args[] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_ATTACHMENT]??'');
286
287

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

293
294
295
296
    /**
     * 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
297
     * Else throw \UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL
298
299
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
300
     *
Marc Egger's avatar
Marc Egger committed
301
302
303
304
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
305
     */
306
    private function sqlValidate(array $fe) {
307
308

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

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

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

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

328
329
330
331
332
333
        // 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
            }
334
335
        }

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

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

341
342
    }

343
    /**
Carsten  Rose's avatar
Carsten Rose committed
344
345
     * Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or
     * `sqlInsert`
346
347
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
348
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
349
     *
350
     * @param bool $flagFeAction indicates of the FE are of type 'native' or 'action'.
351
352
353
     * @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
354
355
356
357
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
358
     */
359
360
    public function doSqlBeforeSlaveAfter(array $fe, $recordId, $flagFeAction) {

361
362
        $rcStatus = ACTION_ELEMENT_NO_CHANGE;

363
364
        $this->feFillStoreVar($fe[FE_FILL_STORE_VAR] ?? '');

365
        // slaveId might be used in sqlBefore: get it first.
Carsten  Rose's avatar
Carsten Rose committed
366
367
368
        if (isset($fe[FE_SLAVE_ID])) {
            // Get the slaveId
            $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]);
369

Carsten  Rose's avatar
Carsten Rose committed
370
371
372
373
            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);
            }
374

Carsten  Rose's avatar
Carsten Rose committed
375
376
377
            if ($slaveId === '' || $slaveId === false) {
                $slaveId = 0;
            }
378

Carsten  Rose's avatar
Carsten Rose committed
379
380
            // Store the slaveId: it's used and replaced in the update statement.
            $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);
381
        }
382

383
384
385
386
        // 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
387
388
389
            $doInsert = ($slaveId == 0);
            $doUpdate = ($slaveId != 0);
            $doDelete = ($slaveId != 0) && !empty($fe[FE_SQL_DELETE]);
390

Carsten  Rose's avatar
Carsten Rose committed
391
392
393
394
395
396
            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;
            }
397

Carsten  Rose's avatar
Carsten Rose committed
398
399
400
401
402
403
404
            // 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;
            }
405

Carsten  Rose's avatar
Carsten Rose committed
406
407
408
409
            if ($doUpdate) {
                $this->evaluate->parse($fe[FE_SQL_UPDATE]);
                $rcStatus = ACTION_ELEMENT_MODIFIED;
            }
410

Carsten  Rose's avatar
Carsten Rose committed
411
412
413
414
415
416
            // Fire a delete query
            if ($doDelete) {
                $this->evaluate->parse($fe[FE_SQL_DELETE]);
                $slaveId = 0;
                $rcStatus = ACTION_ELEMENT_DELETED;
            }
417

Carsten  Rose's avatar
Carsten Rose committed
418
419
420
421
422
            // 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.
                $this->db->sql("UPDATE " . $this->primaryTableName . " SET " . $fe[FE_NAME] . " = $slaveId WHERE id = ? LIMIT 1", ROW_REGULAR, [$recordId]);
            }
423
424
        }

425
426
427
        // If given: fire a $sqlAfter query
        $this->evaluate->parse($fe[FE_SQL_AFTER]);

428
        return $rcStatus;
429
    }
430

431
432
433
434
435
    /**
     * 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
436
437
438
     *
     * @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
439
440
     * @throws \CodeException
     * @throws \UserFormException
441
442
443
444
445
446
447
448
449
450
     */
    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
451

452
453
        return false;
    }
454

Carsten  Rose's avatar
Carsten Rose committed
455
456
457
458
459
    /**
     * 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
460
461
     * @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
462
     * @param string $recordDestinationTable - table name where the records will be duplicated to.
Carsten  Rose's avatar
Carsten Rose committed
463
     * @param string $sub - on the highest level an empty string. It's a filter, value comes from
Carsten  Rose's avatar
Carsten Rose committed
464
     *                                       FE.name, to specify sub-sub copy rules.
Carsten  Rose's avatar
Carsten Rose committed
465
     * @param array $clipboard
Carsten  Rose's avatar
Carsten Rose committed
466
     *
Marc Egger's avatar
Marc Egger committed
467
468
469
470
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
     */
    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.
498
            $lastInsertId = $this->prepareDuplicate($feSpecAction, $formElement, $newValues, $recordSourceTable, $recordDestinationTable, $sub, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
499
500
501
502
503
504
505
506
507
508
509
510
511

            # 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
512
     * @param array $feSpecActionAll - all FE.class='action' - just process 'paste'
513
     * @param array $feSpecAction
Carsten  Rose's avatar
Carsten Rose committed
514
     * @param array $updateRecords - array of records: 'id' is the source.id, all other fields will replace
Carsten  Rose's avatar
Carsten Rose committed
515
     *                                       source columns.
Carsten  Rose's avatar
Carsten Rose committed
516
     * @param        $recordSourceTable - table name from where to copy the source records
Carsten  Rose's avatar
Carsten Rose committed
517
     * @param        $recordDestinationTable - table name where the records will be duplicated to.
Carsten  Rose's avatar
Carsten Rose committed
518
     * @param string $sub - on the highest level an empty string. It's a filter, value comes from
Carsten  Rose's avatar
Carsten Rose committed
519
     *                                       FE.name, to specify sub-sub copy rules.
Carsten  Rose's avatar
Carsten Rose committed
520
     * @param array $clipboard -
Carsten  Rose's avatar
Carsten Rose committed
521
     * @return int - lastInsertId
Marc Egger's avatar
Marc Egger committed
522
523
524
525
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
526
     */
527
528
529
    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
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552

        // 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];

            $rowSrc = $this->db->sql("SELECT * FROM $recordSourceTable WHERE id=?", ROW_EXPECT_1, [$newColumns[COLUMN_ID]]);

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

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

            $lastInsertId = $this->copyRecord($rowSrc, $recordDestinationTable);
            $clipboard[$field] = $lastInsertId;
553
            $translateMap[$newColumns[COLUMN_ID]] = $lastInsertId;
Carsten  Rose's avatar
Carsten Rose committed
554
555
556
557
558
559

            // 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 == "") {
560
                $this->doAllFormElementPaste($feSpecActionAll, $recordSourceTable, $recordDestinationTable, $field, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
561
562
563
            }
        }

564
565
566
567
568
        // 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
569
570
571
572
        return $lastInsertId;

    } // prepareDuplicate()

573
574
575
576
577
578
579
580
581
582
583
584
585
    /**
     * 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
586
587
588
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
     */
    private function translateId(array $translateMap, $translateIdColumn, $tableName) {

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

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

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

        }
    }

Carsten  Rose's avatar
Carsten Rose committed
604
605
606
    /**
     * @param array $rowSrc
     * @param array $rowDest
Carsten  Rose's avatar
Carsten Rose committed
607
     *
Marc Egger's avatar
Marc Egger committed
608
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
     */
    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
624
            HelperFile::mkDirParent($rowDest[$key]);
625
            HelperFile::copy($val, $rowDest[$key]);
Carsten  Rose's avatar
Carsten Rose committed
626
627
628
629
630
631
632
633
634
        }
    }

    /**
     * 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
635
     * @param array $row
Carsten  Rose's avatar
Carsten Rose committed
636
     * @param string $destTable
Carsten  Rose's avatar
Carsten Rose committed
637
     *
Carsten  Rose's avatar
Carsten Rose committed
638
     * @return int - lastInsertId
Marc Egger's avatar
Marc Egger committed
639
640
641
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
642
643
644
645
646
647
648
649
650
651
     */
    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) {
652

Carsten  Rose's avatar
Carsten Rose committed
653
            $key = $col[COLUMN_FIELD];
Carsten  Rose's avatar
Carsten Rose committed
654
            // Only copy columns which exist on source AND destination.
655
656
657
            if (!isset($row[$key])) {
                continue;
            }
Carsten  Rose's avatar
Carsten Rose committed
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
            $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);
        }

        $keyString = implode(',', $keys);
        $valueString = implode(',', $placeholder);

        $sql = "INSERT INTO $destTable ($keyString) VALUES ($valueString)";

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

    } # copyRecord()
690
}