FormAction.php 27 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
93
94
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM);

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

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

102
103
104
            if (isset($fe[FE_FILL_STORE_VAR])) {
                $rows = $this->evaluate->parse($fe[FE_FILL_STORE_VAR]);
                if (is_array($rows)) {
105
                    $this->store->appendToStore($rows[0], STORE_VAR);
106
107
108
109
110
111
                } else {
                    if (!empty($rows)) {
                        throw new UserFormException("Invalid statement for 'fillStoreVar': " . $fe[FE_FILL_STORE_VAR], ERROR_INVALID_OR_MISSING_PARAMETER);
                    }
                }
                $fe[FE_FILL_STORE_VAR] = ''; // do not process the same later on.
112
113
            }

114
115
            // Process templateGroup action elements
            if (isset($fe[FE_ID_CONTAINER]) && $fe[FE_ID_CONTAINER] > 0) {
116
                // Get native 'templateGroup'-FE - to retrieve MAX_LENGTH
117
118
119
120
121
122
                $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.
123
124
//                    for ($ii = $maxCopies; $ii > 0; $ii--) { // Iterate backwards: deleting records starts at the end and doesn't affect remaining counting
                    $correctDeleteIndex = 0;
125
                    for ($ii = 1; $ii <= $maxCopies; $ii++) {
126
127
128
129
130
131
132
133
134
135
136
137
138
                        $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;
139
140
                        }
                    }
141
                    continue; // skip to next FormElement
142
143
                }
            }
144

145
146
147
148
149
150
151
152
            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.
                    $this->fillStoreRecord($this->primaryTableName, $recordId);
153
154
            }

155
156
157
158
            if (!$this->checkRequiredList($fe)) {
                continue;
            }

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

167
                if ($fe[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
168
169
170
171
                    $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);
                }

172
173
174
175
176
                $ldap = new Ldap();
                $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
                $this->store->setStore($arr, STORE_LDAP, true);
            }

177
            if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) {
178
                $this->doSendMail($fe);
179
180
181
                //no further processing of current element necessary.
                continue;
            }
182
183
184

            $this->validate($fe);

185
186
187
188
189
190
191
192
193
            $rcTmp = $this->doSlave($fe, $recordId);
            switch ($rcTmp) {
                case ACTION_ELEMENT_MODIFIED:
                case ACTION_ELEMENT_DELETED:
                    $rc = $rcTmp;
                    break;
                default:
                    break;
            }
194
        }
195

196
        return $rc;
197
198
199
    }

    /**
200
201
     * Copy the current primary record to STORE_RECORD
     *
202
203
     * @param $table
     * @param $recordId
Carsten  Rose's avatar
Carsten Rose committed
204
     *
205
     * @throws CodeException
206
     * @throws DbException
207
208
     * @throws UserFormException
     */
209
    private function fillStoreRecord($table, $recordId) {
210

211
212
        if (!is_string($table) || $table === '') {
            throw new UserFormException("");
213
        }
214

215
216
        if ($recordId !== false && $recordId > 0) {
            $record = $this->db->sql("SELECT * FROM $table WHERE id = ?", ROW_EXPECT_1, [$recordId]);
217
            $this->store->setStore($record, STORE_RECORD, true);
218
        }
219
    }
220

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

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

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

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

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

249
        return true;
250
251
    }

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

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

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

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

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

317
        if ($fe[FE_MESSAGE_FAIL] === '') {
318
            throw new UserFormException("Missing parameter '" . FE_MESSAGE_FAIL . "'", ERROR_MISSING_MESSAGE_FAIL);
319
320
321
322
323
324
325
326
        }

        // Do the check
        $result = $this->evaluate->parse($fe[FE_SQL_VALIDATE]);
        if (!is_array($result)) {
            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
        // 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
            }
333
334
        }

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

337
        // Throw user error message
338
339
340
        throw new UserFormException($msg, ERROR_REPORT_FAILED_ACTION);
    }

341
    /**
Carsten  Rose's avatar
Carsten Rose committed
342
343
     * Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or
     * `sqlInsert`
344
345
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
346
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
347
     *
348
349
350
351
     * @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
     *              $sqlAfter won't affect the $rc.
352
     * @throws CodeException
353
     * @throws DbException
354
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
355
     * @throws UserReportException
356
     */
357
    private function doSlave(array $fe, $recordId) {
358

359
360
        $rcStatus = ACTION_ELEMENT_NO_CHANGE;

361
        // Get the slaveId
362
        $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]);
363
364
365
366
367
368
369
370
371
372
373

        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.
374
375
376
377
        $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);

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

379
380
        $doInsert = ($slaveId == 0);
        $doUpdate = ($slaveId != 0);
381
        $doDelete = ($slaveId != 0) && $fe[FE_SQL_DELETE] != '';
382
383
384
385
386
387
388
389
390

        $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;
        }

391
        // Fire slave query
392
        if ($doInsert) {
393
            $slaveId = $this->evaluate->parse($fe[FE_SQL_INSERT]);
394
395
            // Store the slaveId: might be used later
            $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);
396
            $rcStatus = ACTION_ELEMENT_MODIFIED;
397
398
399
        }

        if ($doUpdate) {
400
            $this->evaluate->parse($fe[FE_SQL_UPDATE]);
401
            $rcStatus = ACTION_ELEMENT_MODIFIED;
402
403
        }

404
405
406
407
        // Fire a delete query
        if ($doDelete) {
            $this->evaluate->parse($fe[FE_SQL_DELETE]);
            $slaveId = 0;
408
            $rcStatus = ACTION_ELEMENT_DELETED;
409
410
        }

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

417
        // If given: fire a $sqlAfter query. $sqlAfter won't affect $rc
418
419
        $this->evaluate->parse($fe[FE_SQL_AFTER]);

420
        return $rcStatus;
421
    }
422

423
424
425
426
427
    /**
     * 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
428
429
430
     *
     * @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)
431
     * @throws CodeException
Carsten  Rose's avatar
Carsten Rose committed
432
     * @throws UserFormException
433
434
435
436
437
438
439
440
441
442
     */
    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
443

444
445
        return false;
    }
446

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

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

        // 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;
545
            $translateMap[$newColumns[COLUMN_ID]] = $lastInsertId;
Carsten  Rose's avatar
Carsten Rose committed
546
547
548
549
550
551

            // 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 == "") {
552
                $this->doAllFormElementPaste($feSpecActionAll, $recordSourceTable, $recordDestinationTable, $field, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
553
554
555
            }
        }

556
557
558
559
560
        // 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
561
562
563
564
        return $lastInsertId;

    } // prepareDuplicate()

565
566
567
568
569
570
571
572
573
574
575
576
577
    /**
     * 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
578
579
     * @throws CodeException
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
580
     * @throws UserFormException
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
     */
    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
596
597
598
    /**
     * @param array $rowSrc
     * @param array $rowDest
Carsten  Rose's avatar
Carsten Rose committed
599
     *
Carsten  Rose's avatar
Carsten Rose committed
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
     * @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;
            }

            Support::mkDirParent($rowDest[$key]);
            if (!copy($val, $rowDest[$key])) {
                throw new UserFormException("Error copy file from [$val] to [" . $rowDest[$key] . "]", ERROR_IO_COPY_FILE);
            }
        }
    }

    /**
     * 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
629
     * @param array $row
Carsten  Rose's avatar
Carsten Rose committed
630
     * @param string $destTable
Carsten  Rose's avatar
Carsten Rose committed
631
     *
Carsten  Rose's avatar
Carsten Rose committed
632
633
634
     * @return int - lastInsertId
     * @throws CodeException
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
635
     * @throws UserFormException
Carsten  Rose's avatar
Carsten Rose committed
636
637
638
639
640
641
642
643
644
645
     */
    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) {
646

Carsten  Rose's avatar
Carsten Rose committed
647
            $key = $col[COLUMN_FIELD];
Carsten  Rose's avatar
Carsten Rose committed
648
            // Only copy columns which exist on source AND destination.
649
650
651
            if (!isset($row[$key])) {
                continue;
            }
Carsten  Rose's avatar
Carsten Rose committed
652
653
654
655
656
657
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
            $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()
684
}