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

namespace qfq;

require_once(__DIR__ . '/../Constants.php');
12
require_once(__DIR__ . '/../database/Database.php');
13
14
require_once(__DIR__ . '/../store/Store.php');
require_once(__DIR__ . '/../Evaluate.php');
15
require_once(__DIR__ . '/../report/SendMail.php');
16
require_once(__DIR__ . '/../helper/HelperFormElement.php');
17
require_once(__DIR__ . '/../exceptions/UserFormException.php');
18

19

20
21
22
23
/**
 * Class formAction
 * @package qfq
 */
24
class FormAction {
25
26

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

32
    private $formSpec = array();
33
    private $primaryTableName = '';
Carsten  Rose's avatar
Carsten Rose committed
34

35
36
37
38
    /**
     * @var Database
     */
    private $db = null;
Carsten  Rose's avatar
Carsten Rose committed
39

40
41
42
43
44
45
46
47
48
    /**
     * @var Store
     */
    private $store = null;

    /**
     * @param array $formSpec
     * @param Database $db
     * @param bool|false $phpUnit
49
50
     * @throws CodeException
     * @throws UserFormException
51
     * @throws UserReportException
52
53
54
     */
    public function __construct(array $formSpec, Database $db, $phpUnit = false) {
        $this->formSpec = $formSpec;
55
        $this->primaryTableName = Support::setIfNotSet($formSpec, F_TABLE_NAME);
56
57
58
59
60
61
62
63
64
65
66

        $this->db = $db;

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

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

    }

    /**
     * @param integer $recordId
Carsten  Rose's avatar
Carsten Rose committed
67
68
     * @param array $feSpecAction
     * @param string $feTypeList
69
70
71
     *         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
72
     *
73
74
75
     * @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)
76
77
     * @throws CodeException
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
78
     * @throws DownloadException
79
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
80
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
81
82
83
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
84
     */
85
    public function elements($recordId, array $feSpecAction, $feTypeList) {
86

87
        $rc = ACTION_ELEMENT_NO_CHANGE;
88

89
90
91
        // Iterate over all Action FormElements
        foreach ($feSpecAction as $fe) {

92
            // Preparation for Log, Debug
93
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); // debug
94
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID]??'', STORE_SYSTEM); // debug
95

96
            $fe = HelperFormElement::initActionFormElement($fe);
97

98
            // Only process FE elements of types listed in $feTypeList. Skip all other
99
100
101
102
            if (false === Support::findInSet($fe[FE_TYPE], $feTypeList)) {
                continue;
            }

103
            if (isset($fe[FE_FILL_STORE_VAR])) {
104
                $this->store->setVar(SYSTEM_FORM_ELEMENT_COLUMN, FE_FILL_STORE_VAR, STORE_SYSTEM); // debug
105
                $rows = $this->evaluate->parse($fe[FE_FILL_STORE_VAR], ROW_EXPECT_0_1);
106
                if (is_array($rows)) {
107
                    $this->store->appendToStore($rows, STORE_VAR);
108
109
                } else {
                    if (!empty($rows)) {
110
111
                        throw new UserFormException(json_encode(
                            [ERROR_MESSAGE_TO_USER => "Invalid statement for 'fillStoreVar'.",
Marc Egger's avatar
Marc Egger committed
112
                                ERROR_MESSAGE_TO_DEVELOPER => $fe[FE_FILL_STORE_VAR]]), ERROR_INVALID_OR_MISSING_PARAMETER);
113
114
115
                    }
                }
                $fe[FE_FILL_STORE_VAR] = ''; // do not process the same later on.
116
117
            }

118
119
            // Process templateGroup action elements
            if (isset($fe[FE_ID_CONTAINER]) && $fe[FE_ID_CONTAINER] > 0) {
120
                // Get native 'templateGroup'-FE - to retrieve MAX_LENGTH
121
122
123
124
125
126
                $templateGroup = $this->db->sql(SQL_FORM_ELEMENT_TEMPLATE_GROUP_FE_ID, ROW_EXPECT_1, [$fe[FE_ID_CONTAINER]],
                    "In FormElement.id=" . $fe[FE_ID] . ", feIdContainer=" . $fe[FE_ID_CONTAINER] . '  should point to a templateGroup');

                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.
127
128
//                    for ($ii = $maxCopies; $ii > 0; $ii--) { // Iterate backwards: deleting records starts at the end and doesn't affect remaining counting
                    $correctDeleteIndex = 0;
129
                    for ($ii = 1; $ii <= $maxCopies; $ii++) {
130
131
132
133
134
135
136
137
138
139
140
141
142
                        $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;
143
144
                        }
                    }
145
                    continue; // skip to next FormElement
146
147
                }
            }
148

149
150
151
152
153
154
155
            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.
156
                    $this->store->fillStoreWithRecord($this->primaryTableName, $recordId, $this->db, $this->formSpec[F_PRIMARY_KEY]??'');
157
158
            }

159
160
161
162
            if (!$this->checkRequiredList($fe)) {
                continue;
            }

163
164
165
166
167
            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
168
                $config = OnArray::getArrayItems($fe, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES, FE_LDAP_USE_BIND_CREDENTIALS]);
169
170
                $config = $this->evaluate->parseArray($config);

171
                if ($fe[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
172
173
174
175
                    $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);
                }

176
177
178
179
180
                $ldap = new Ldap();
                $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
                $this->store->setStore($arr, STORE_LDAP, true);
            }

181
            $this->sqlValidate($fe);
182

183
184
185
            // If given: fire a sqlBefore query
            $this->evaluate->parse($fe[FE_SQL_BEFORE]);

186
            if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) {
187
                $this->doSendMail($fe);
188
189
190
191
192
193
194
195
196
197
            } else {
                $rcTmp = $this->doSlave($fe, $recordId);
                switch ($rcTmp) {
                    case ACTION_ELEMENT_MODIFIED:
                    case ACTION_ELEMENT_DELETED:
                        $rc = $rcTmp;
                        break;
                    default:
                        break;
                }
198
            }
199

200
201
            // If given: fire a $sqlAfter query
            $this->evaluate->parse($fe[FE_SQL_AFTER]);
202
        }
203

204
        return $rc;
205
206
    }

207
    /**
208
     * Process all FormElements given in the `requiredList` identified by their name.
209
210
211
212
     * 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
213
     *
214
     * @return bool  true if none FE is specified or all specified are non empty.
215
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
216
     * @throws UserFormException
217
218
     */
    private function checkRequiredList(array $fe) {
219

220
221
        if (!isset($fe[FE_REQUIRED_LIST]) || $fe[FE_REQUIRED_LIST] === '') {
            return true;
222
223
        }

224
225
226
227
        $arr = explode(',', $fe[FE_REQUIRED_LIST]);
        foreach ($arr as $key) {

            $key = trim($key);
228
            $val = $this->store->getVar($key, STORE_FORM, SANITIZE_ALLOW_ALL);
229
230
231
232

            if ($val === false || $val === '' || $val === '0') {
                return false;
            }
233
234
        }

235
        return true;
236
237
    }

238
239
    /**
     * @param array $feSpecAction
240
241
     * @throws CodeException
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
242
     * @throws DownloadException
243
244
     * @throws UserFormException
     * @throws UserReportException
245
246
247
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
248
     */
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
    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]);
267
268
269
        $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]);
270
        $args[] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_ATTACHMENT]??'');
271
272

        // Mail: send
273
274
275
        $sendMail = new SendMail();
        $mailConfig = $sendMail->parseStringToArray(implode(PARAM_DELIMITER, $args));
        $sendMail->process($mailConfig);
276
277
    }

278
279
280
281
    /**
     * 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.
282
     * Else throw UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL
283
284
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
285
     *
Carsten  Rose's avatar
Carsten Rose committed
286
287
     * @throws CodeException
     * @throws DbException
288
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
289
     * @throws UserReportException
290
     */
291
    private function sqlValidate(array $fe) {
292
293

        // Is there something to check?
294
        if ($fe[FE_SQL_VALIDATE] === '') {
295
296
297
            return;
        }

298
        if ($fe[FE_EXPECT_RECORDS] === '') {
299
300
            throw new UserFormException("Missing parameter '" . FE_EXPECT_RECORDS . "'", ERROR_MISSING_EXPECT_RECORDS);
        }
301
        $expect = $this->evaluate->parse($fe[FE_EXPECT_RECORDS]);
302

303
        if ($fe[FE_MESSAGE_FAIL] === '') {
304
            throw new UserFormException("Missing parameter '" . FE_MESSAGE_FAIL . "'", ERROR_MISSING_MESSAGE_FAIL);
305
306
307
        }

        // Do the check
308
        $result = $this->evaluate->parse($fe[FE_SQL_VALIDATE], ROW_REGULAR);
309
310
311
312
        if (!is_array($result)) {
            throw new UserFormException("Expected an array for '" . FE_SQL_VALIDATE . "', got a scalar. Please check for {{!...", ERROR_EXPECTED_ARRAY);
        }

313
314
315
316
317
318
        // 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
            }
319
320
        }

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

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

326
327
    }

328
    /**
Carsten  Rose's avatar
Carsten Rose committed
329
330
     * Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or
     * `sqlInsert`
331
332
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
333
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
334
     *
335
336
337
     * @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
338
     * @throws CodeException
339
     * @throws DbException
340
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
341
     * @throws UserReportException
342
     */
343
    private function doSlave(array $fe, $recordId) {
344

345
346
        $rcStatus = ACTION_ELEMENT_NO_CHANGE;

347
        // Get the slaveId
348
        $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]);
349
350
351
352
353
354
355
356
357
358
359

        if ($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);
        }

        if ($slaveId === '' || $slaveId === false) {
            $slaveId = 0;
        }

        // Store the slaveId: it's used and replaced in the update statement.
360
361
        $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);

362
363
        $doInsert = ($slaveId == 0);
        $doUpdate = ($slaveId != 0);
364
        $doDelete = ($slaveId != 0) && $fe[FE_SQL_DELETE] != '';
365
366
367
368
369
370
371
372
373

        $flagHonor = isset($fe[FE_SQL_HONOR_FORM_ELEMENTS]) && $fe[FE_SQL_HONOR_FORM_ELEMENTS] != '';
        if ($flagHonor) {
            $filled = $this->checkFormElements($fe[FE_SQL_HONOR_FORM_ELEMENTS]);
            $doInsert = $filled && $doInsert;
            $doUpdate = $filled && $doUpdate;
            $doDelete = !$filled && $doDelete;
        }

374
        // Fire slave query
375
        if ($doInsert) {
376
            $slaveId = $this->evaluate->parse($fe[FE_SQL_INSERT]);
377
378
            // Store the slaveId: might be used later
            $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);
379
            $rcStatus = ACTION_ELEMENT_MODIFIED;
380
381
382
        }

        if ($doUpdate) {
383
            $this->evaluate->parse($fe[FE_SQL_UPDATE]);
384
            $rcStatus = ACTION_ELEMENT_MODIFIED;
385
386
        }

387
388
389
390
        // Fire a delete query
        if ($doDelete) {
            $this->evaluate->parse($fe[FE_SQL_DELETE]);
            $slaveId = 0;
391
            $rcStatus = ACTION_ELEMENT_DELETED;
392
393
        }

394
395
        // Check if there is a column with the same name as the 'action'-FormElement.
        if (false !== $this->store->getVar($fe[FE_NAME], STORE_RECORD)) {
396
            // After an insert or update, propagate the (new) slave id to the master record.
397
398
399
            $this->db->sql("UPDATE " . $this->primaryTableName . " SET " . $fe[FE_NAME] . " = $slaveId WHERE id = ? LIMIT 1", ROW_REGULAR, [$recordId]);
        }

400
        return $rcStatus;
401
    }
402

403
404
405
406
407
    /**
     * 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
408
409
410
     *
     * @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)
411
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
412
     * @throws UserFormException
413
414
415
416
417
418
419
420
421
422
     */
    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
423

424
425
        return false;
    }
426

Carsten  Rose's avatar
Carsten Rose committed
427
428
429
430
431
    /**
     * 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
432
433
     * @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
434
     * @param string $recordDestinationTable - table name where the records will be duplicated to.
Carsten  Rose's avatar
Carsten Rose committed
435
     * @param string $sub - on the highest level an empty string. It's a filter, value comes from
Carsten  Rose's avatar
Carsten Rose committed
436
     *                                       FE.name, to specify sub-sub copy rules.
Carsten  Rose's avatar
Carsten Rose committed
437
     * @param array $clipboard
Carsten  Rose's avatar
Carsten Rose committed
438
     *
Carsten  Rose's avatar
Carsten Rose committed
439
     * @throws CodeException
440
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
441
     * @throws UserFormException
442
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
     */
    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.
470
            $lastInsertId = $this->prepareDuplicate($feSpecAction, $formElement, $newValues, $recordSourceTable, $recordDestinationTable, $sub, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
471
472
473
474
475
476
477
478
479
480
481
482
483

            # 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
484
     * @param array $feSpecActionAll - all FE.class='action' - just process 'paste'
485
     * @param array $feSpecAction
Carsten  Rose's avatar
Carsten Rose committed
486
     * @param array $updateRecords - array of records: 'id' is the source.id, all other fields will replace
Carsten  Rose's avatar
Carsten Rose committed
487
     *                                       source columns.
Carsten  Rose's avatar
Carsten Rose committed
488
     * @param        $recordSourceTable - table name from where to copy the source records
Carsten  Rose's avatar
Carsten Rose committed
489
     * @param        $recordDestinationTable - table name where the records will be duplicated to.
Carsten  Rose's avatar
Carsten Rose committed
490
     * @param string $sub - on the highest level an empty string. It's a filter, value comes from
Carsten  Rose's avatar
Carsten Rose committed
491
     *                                       FE.name, to specify sub-sub copy rules.
Carsten  Rose's avatar
Carsten Rose committed
492
     * @param array $clipboard -
Carsten  Rose's avatar
Carsten Rose committed
493
494
495
496
     * @return int - lastInsertId
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
497
     * @throws UserReportException
Carsten  Rose's avatar
Carsten Rose committed
498
     */
499
500
501
    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
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524

        // 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;
525
            $translateMap[$newColumns[COLUMN_ID]] = $lastInsertId;
Carsten  Rose's avatar
Carsten Rose committed
526
527
528
529
530
531

            // 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 == "") {
532
                $this->doAllFormElementPaste($feSpecActionAll, $recordSourceTable, $recordDestinationTable, $field, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
533
534
535
            }
        }

536
537
538
539
540
        // 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
541
542
543
544
        return $lastInsertId;

    } // prepareDuplicate()

545
546
547
548
549
550
551
552
553
554
555
556
557
    /**
     * 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
558
559
     * @throws CodeException
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
560
     * @throws UserFormException
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
     */
    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
576
577
578
    /**
     * @param array $rowSrc
     * @param array $rowDest
Carsten  Rose's avatar
Carsten Rose committed
579
     *
Carsten  Rose's avatar
Carsten Rose committed
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
     * @throws UserFormException
     */
    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
596
            HelperFile::mkDirParent($rowDest[$key]);
597
            HelperFile::copy($val, $rowDest[$key]);
Carsten  Rose's avatar
Carsten Rose committed
598
599
600
601
602
603
604
605
606
        }
    }

    /**
     * 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
607
     * @param array $row
Carsten  Rose's avatar
Carsten Rose committed
608
     * @param string $destTable
Carsten  Rose's avatar
Carsten Rose committed
609
     *
Carsten  Rose's avatar
Carsten Rose committed
610
611
612
     * @return int - lastInsertId
     * @throws CodeException
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
613
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
614
615
616
617
618
619
620
621
622
623
     */
    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) {
624

Carsten  Rose's avatar
Carsten Rose committed
625
            $key = $col[COLUMN_FIELD];
Carsten  Rose's avatar
Carsten Rose committed
626
            // Only copy columns which exist on source AND destination.
627
628
629
            if (!isset($row[$key])) {
                continue;
            }
Carsten  Rose's avatar
Carsten Rose committed
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
            $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()
662
}