FormAction.php 25.3 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
31
    /**
     * @var Evaluate instantiated class
     */
    protected $evaluate = null;  // copy of the loaded form
    private $formSpec = array();
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    private $primaryTableName = '';
    /**
     * @var Database
     */
    private $db = null;
    /**
     * @var Store
     */
    private $store = null;

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

        $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
61
62
     * @param array $feSpecAction
     * @param string $feTypeList
63
64
65
     *         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
66
     *
67
68
69
     * @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)
70
71
72
73
     * @throws CodeException
     * @throws DbException
     * @throws UserFormException
     */
74
    public function elements($recordId, array $feSpecAction, $feTypeList) {
75

76
        $rc = ACTION_ELEMENT_NO_CHANGE;
77

78
79
80
        // Iterate over all Action FormElements
        foreach ($feSpecAction as $fe) {

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

84
            $fe = HelperFormElement::initActionFormElement($fe);
85

86
            // Only process FE elements of types listed in $feTypeList. Skip all other
87
88
89
90
            if (false === Support::findInSet($fe[FE_TYPE], $feTypeList)) {
                continue;
            }

91
92
93
94
95
96
97
98
99
100
            if (isset($fe[FE_FILL_STORE_VAR])) {
                $rows = $this->evaluate->parse($fe[FE_FILL_STORE_VAR]);
                if (is_array($rows)) {
                    $this->store->appendToStore(STORE_VAR, $rows[0]);
                } 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.
101
102
            }

103
104
            // Process templateGroup action elements
            if (isset($fe[FE_ID_CONTAINER]) && $fe[FE_ID_CONTAINER] > 0) {
105
                // Get native 'templateGroup'-FE - to retrieve MAX_LENGTH
106
107
108
109
110
111
                $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.
112
113
//                    for ($ii = $maxCopies; $ii > 0; $ii--) { // Iterate backwards: deleting records starts at the end and doesn't affect remaining counting
                    $correctDeleteIndex = 0;
114
                    for ($ii = 1; $ii <= $maxCopies; $ii++) {
115
116
117
118
119
120
121
122
123
124
125
126
127
                        $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;
128
129
                        }
                    }
130
                    continue; // skip to next FormElement
131
132
                }
            }
133

134
135
136
137
138
139
140
141
            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);
142
143
            }

144
145
146
147
            if (!$this->checkRequiredList($fe)) {
                continue;
            }

148
149
150
151
152
            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
153
                $config = OnArray::getArrayItems($fe, [FE_LDAP_SERVER, FE_LDAP_BASE_DN, FE_LDAP_SEARCH, FE_LDAP_ATTRIBUTES, FE_LDAP_USE_BIND_CREDENTIALS]);
154
155
                $config = $this->evaluate->parseArray($config);

156
                if ($fe[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
157
158
159
160
                    $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);
                }

161
162
163
164
165
                $ldap = new Ldap();
                $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
                $this->store->setStore($arr, STORE_LDAP, true);
            }

166
167
168
169
170
            if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) {
                $this->sendMail($fe);
                //no further processing of current element necessary.
                continue;
            }
171
172
173

            $this->validate($fe);

174
175
176
177
178
179
180
181
182
            $rcTmp = $this->doSlave($fe, $recordId);
            switch ($rcTmp) {
                case ACTION_ELEMENT_MODIFIED:
                case ACTION_ELEMENT_DELETED:
                    $rc = $rcTmp;
                    break;
                default:
                    break;
            }
183
        }
184

185
        return $rc;
186
187
188
    }

    /**
189
190
     * Copy the current primary record to STORE_RECORD
     *
191
192
     * @param $table
     * @param $recordId
Carsten  Rose's avatar
Carsten Rose committed
193
     *
194
     * @throws CodeException
195
     * @throws DbException
196
197
     * @throws UserFormException
     */
198
    private function fillStoreRecord($table, $recordId) {
199

200
201
        if (!is_string($table) || $table === '') {
            throw new UserFormException("");
202
        }
203

204
205
        if ($recordId !== false && $recordId > 0) {
            $record = $this->db->sql("SELECT * FROM $table WHERE id = ?", ROW_EXPECT_1, [$recordId]);
206
            $this->store->setStore($record, STORE_RECORD, true);
207
        }
208
    }
209

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

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

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

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

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

236
        return true;
237
238
    }

239
240
241
242
243
244
245
246
247
248
    /**
     * @param array $feSpecAction
     */
    private function sendMail(array $feSpecAction) {

        $mail[SENDMAIL_IDX_RECEIVER] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_TO]);
        $mail[SENDMAIL_IDX_SENDER] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FROM]);
        $mail[SENDMAIL_IDX_SUBJECT] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_SUBJECT]);
        $mail[SENDMAIL_IDX_BODY] = $this->evaluate->parse($feSpecAction[FE_VALUE]);
        $mail[SENDMAIL_IDX_REPLY_TO] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_REPLY_TO]);
249
        $mail[SENDMAIL_IDX_FLAG_AUTO_SUBMIT] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FLAG_AUTO_SUBMIT]) === 'off' ? 'off' : 'on';
250
251
252
253
        $mail[SENDMAIL_IDX_GR_ID] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_GR_ID]);
        $mail[SENDMAIL_IDX_X_ID] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID]);
        $mail[SENDMAIL_IDX_RECEIVER_CC] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_CC]);
        $mail[SENDMAIL_IDX_RECEIVER_BCC] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BCC]);
Carsten  Rose's avatar
Carsten Rose committed
254
        $mail[SENDMAIL_IDX_SRC] = "FormId: " . $feSpecAction[FE_FORM_ID] . ", FormElementId: " . $feSpecAction['id'];
255
256
257
258
259

        // Mail: send
        new Sendmail($mail);
    }

260
261
262
263
    /**
     * 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.
264
     * Else throw UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL
265
266
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
267
     *
268
269
270
271
272
     * @throws UserFormException
     */
    private function validate(array $fe) {

        // Is there something to check?
273
        if ($fe[FE_SQL_VALIDATE] === '') {
274
275
276
            return;
        }

277
        $expect = $this->evaluate->parse($fe[FE_EXPECT_RECORDS]);
278

279
        if ($fe[FE_MESSAGE_FAIL] === '') {
280
281
282
283
284
285
286
287
288
            throw new UserFormException("Missing error message. Column: " . FE_MESSAGE_FAIL, ERROR_MISSING_MESSAGE_FAIL);
        }

        // 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);
        }

289
290
291
292
293
294
        // 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
            }
295
296
        }

297
        $msg = $this->evaluate->parse($fe[FE_MESSAGE_FAIL]); // Replace possible dynamic parts
298
299
300
301
302

        // Throw user defineable error message
        throw new UserFormException($msg, ERROR_REPORT_FAILED_ACTION);
    }

303
    /**
Carsten  Rose's avatar
Carsten Rose committed
304
305
     * Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or
     * `sqlInsert`
306
307
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
308
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
309
     *
310
311
312
313
     * @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.
314
     * @throws CodeException
315
     * @throws DbException
316
317
     * @throws UserFormException
     */
318
    private function doSlave(array $fe, $recordId) {
319

320
321
        $rcStatus = ACTION_ELEMENT_NO_CHANGE;

322
        // Get the slaveId
323
        $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]);
324
325
326
327
328
329
330
331
332
333
334

        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.
335
336
337
338
        $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);

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

340
341
        $doInsert = ($slaveId == 0);
        $doUpdate = ($slaveId != 0);
342
        $doDelete = ($slaveId != 0) && $fe[FE_SQL_DELETE] != '';
343
344
345
346
347
348
349
350
351

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

352
        // Fire slave query
353
        if ($doInsert) {
354
            $slaveId = $this->evaluate->parse($fe[FE_SQL_INSERT]);
355
356
            // Store the slaveId: might be used later
            $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);
357
            $rcStatus = ACTION_ELEMENT_MODIFIED;
358
359
360
        }

        if ($doUpdate) {
361
            $this->evaluate->parse($fe[FE_SQL_UPDATE]);
362
            $rcStatus = ACTION_ELEMENT_MODIFIED;
363
364
        }

365
366
367
368
        // Fire a delete query
        if ($doDelete) {
            $this->evaluate->parse($fe[FE_SQL_DELETE]);
            $slaveId = 0;
369
            $rcStatus = ACTION_ELEMENT_DELETED;
370
371
        }

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

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

381
        return $rcStatus;
382
    }
383

384
385
386
387
388
    /**
     * 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
389
390
391
     *
     * @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)
392
393
394
395
396
397
398
399
400
401
     */
    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
402

403
404
        return false;
    }
405

406
407
408
409
    /**
     * Set all necessary keys - subsequent 'isset()' are not necessary anymore.
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
410
     *
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
     * @return array
     */
    private function initActionFormElement(array $fe) {

        $list = [FE_TYPE, FE_SLAVE_ID, FE_SQL_VALIDATE, FE_SQL_BEFORE, FE_SQL_INSERT, FE_SQL_UPDATE, FE_SQL_DELETE,
            FE_SQL_AFTER, FE_EXPECT_RECORDS, FE_REQUIRED_LIST, FE_MESSAGE_FAIL, FE_SENDMAIL_TO, FE_SENDMAIL_CC,
            FE_SENDMAIL_BCC, FE_SENDMAIL_FROM, FE_SENDMAIL_SUBJECT, FE_SENDMAIL_REPLY_TO, FE_SENDMAIL_FLAG_AUTO_SUBMIT,
            FE_SENDMAIL_GR_ID, FE_SENDMAIL_X_ID];

        foreach ($list as $key) {
            Support::setIfNotSet($fe, $key);
        }

        return $fe;
    }
Carsten  Rose's avatar
Carsten Rose committed
426
427
428
429
430
431
432


    /**
     * 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
433
434
     * @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
435
     * @param string $recordDestinationTable - table name where the records will be duplicated to.
Carsten  Rose's avatar
Carsten Rose committed
436
     * @param string $sub - on the highest level an empty string. It's a filter, value comes from
Carsten  Rose's avatar
Carsten Rose committed
437
     *                                       FE.name, to specify sub-sub copy rules.
Carsten  Rose's avatar
Carsten Rose committed
438
     * @param array $clipboard
Carsten  Rose's avatar
Carsten Rose committed
439
     *
Carsten  Rose's avatar
Carsten Rose committed
440
441
442
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
     * @throws CodeException
     * @throws UserFormException
     */
    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.
469
            $lastInsertId = $this->prepareDuplicate($feSpecAction, $formElement, $newValues, $recordSourceTable, $recordDestinationTable, $sub, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
470
471
472
473
474
475
476
477
478
479
480
481
482

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

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

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

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

    } // prepareDuplicate()

544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
    /**
     * 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
     */
    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
572
573
574
    /**
     * @param array $rowSrc
     * @param array $rowDest
Carsten  Rose's avatar
Carsten Rose committed
575
     *
Carsten  Rose's avatar
Carsten Rose committed
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
     * @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
605
     * @param array $row
Carsten  Rose's avatar
Carsten Rose committed
606
     * @param string $destTable
Carsten  Rose's avatar
Carsten Rose committed
607
     *
Carsten  Rose's avatar
Carsten Rose committed
608
609
610
611
612
613
614
615
616
617
618
619
620
     * @return int - lastInsertId
     * @throws CodeException
     * @throws DbException
     */
    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) {
621

Carsten  Rose's avatar
Carsten Rose committed
622
            $key = $col[COLUMN_FIELD];
Carsten  Rose's avatar
Carsten Rose committed
623
            // Only copy columns which exist on source AND destination.
624
625
626
            if (!isset($row[$key])) {
                continue;
            }
Carsten  Rose's avatar
Carsten Rose committed
627
628
629
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
            $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()
659
}