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

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

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

23

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

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

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

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

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

    /**
     * @param array $formSpec
     * @param Database $db
     * @param bool|false $phpUnit
Marc Egger's avatar
Marc Egger committed
53
54
55
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
56
57
58
     */
    public function __construct(array $formSpec, Database $db, $phpUnit = false) {
        $this->formSpec = $formSpec;
59
        $this->primaryTableName = Support::setIfNotSet($formSpec, F_TABLE_NAME);
60
61
62
63
64
65
66
67
68
69
70

        $this->db = $db;

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

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

    }

    /**
     * @param integer $recordId
Carsten  Rose's avatar
Carsten Rose committed
71
72
     * @param array $feSpecAction
     * @param string $feTypeList
73
74
75
     *         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
76
     *
77
78
79
     * @return int: ACTION_ELEMENT_MODIFIED if there are potential changes on the DB like fired SQL statements,
     *              ACTION_ELEMENT_NO_CHANGE if nothing happened
     *              ACTION_ELEMENT_DELETED:  if a record has been deleted (only in recursive calls, not the initial one)
Marc Egger's avatar
Marc Egger committed
80
81
82
83
84
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
Carsten  Rose's avatar
Carsten Rose committed
85
86
87
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
88
     */
89
    public function elements($recordId, array $feSpecAction, $feTypeList) {
90

91
        $rc = ACTION_ELEMENT_NO_CHANGE;
92

93
94
95
        // Iterate over all Action FormElements
        foreach ($feSpecAction as $fe) {

96
            // Preparation for Log, Debug
97
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($fe), STORE_SYSTEM); // debug
98
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $fe[FE_ID]??'', STORE_SYSTEM); // debug
99

100
            $fe = HelperFormElement::initActionFormElement($fe);
101

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

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

122
123
            // Process templateGroup action elements
            if (isset($fe[FE_ID_CONTAINER]) && $fe[FE_ID_CONTAINER] > 0) {
124
                // Get native 'templateGroup'-FE - to retrieve MAX_LENGTH
125
                $templateGroup = $this->db->sql(SQL_FORM_ELEMENT_TEMPLATE_GROUP_FE_ID, ROW_EXPECT_1, [$fe[FE_ID_CONTAINER]],
126
                    "Action FormElements should not be assigned to a container (exception: templateGroup). FormElement.id=" . $fe[FE_ID] . ", feIdContainer=" . $fe[FE_ID_CONTAINER] . ' is not a templateGroup');
127
128
129
130

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

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

163
164
165
166
            if (!$this->checkRequiredList($fe)) {
                continue;
            }

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

175
                if ($fe[FE_LDAP_USE_BIND_CREDENTIALS] == 1) {
176
177
178
179
                    $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);
                }

180
181
182
183
184
                $ldap = new Ldap();
                $arr = $ldap->process($config, '', MODE_LDAP_SINGLE);
                $this->store->setStore($arr, STORE_LDAP, true);
            }

185
            $this->sqlValidate($fe);
186

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

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

204
205
            // If given: fire a $sqlAfter query
            $this->evaluate->parse($fe[FE_SQL_AFTER]);
206
        }
207

208
        return $rc;
209
210
    }

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

224
225
        if (!isset($fe[FE_REQUIRED_LIST]) || $fe[FE_REQUIRED_LIST] === '') {
            return true;
226
227
        }

228
229
230
231
        $arr = explode(',', $fe[FE_REQUIRED_LIST]);
        foreach ($arr as $key) {

            $key = trim($key);
232
            $val = $this->store->getVar($key, STORE_FORM, SANITIZE_ALLOW_ALL);
233
234
235
236

            if ($val === false || $val === '' || $val === '0') {
                return false;
            }
237
238
        }

239
        return true;
240
241
    }

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

        // Mail: send
277
278
279
        $sendMail = new SendMail();
        $mailConfig = $sendMail->parseStringToArray(implode(PARAM_DELIMITER, $args));
        $sendMail->process($mailConfig);
280
281
    }

282
283
284
285
    /**
     * If there is a query defined in fe.parameter.FE_SQL_VALIDATE: fire them.
     * Count the selected records and compare them with fe.parameter.FE_EXPECT_RECORDS.
     * If match: everything is fine, do nothing.
Marc Egger's avatar
Marc Egger committed
286
     * Else throw \UserFormException with error message of fe.parameter.FE_MESSAGE_FAIL
287
288
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
289
     *
Marc Egger's avatar
Marc Egger committed
290
291
292
293
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
294
     */
295
    private function sqlValidate(array $fe) {
296
297

        // Is there something to check?
298
        if ($fe[FE_SQL_VALIDATE] === '') {
299
300
301
            return;
        }

302
        if ($fe[FE_EXPECT_RECORDS] === '') {
Marc Egger's avatar
Marc Egger committed
303
            throw new \UserFormException("Missing parameter '" . FE_EXPECT_RECORDS . "'", ERROR_MISSING_EXPECT_RECORDS);
304
        }
305
        $expect = $this->evaluate->parse($fe[FE_EXPECT_RECORDS]);
306

307
        if ($fe[FE_MESSAGE_FAIL] === '') {
Marc Egger's avatar
Marc Egger committed
308
            throw new \UserFormException("Missing parameter '" . FE_MESSAGE_FAIL . "'", ERROR_MISSING_MESSAGE_FAIL);
309
310
311
        }

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

317
318
319
320
321
322
        // 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
            }
323
324
        }

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

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

330
331
    }

332
    /**
Carsten  Rose's avatar
Carsten Rose committed
333
334
     * Create the slave record. First try to evaluate a slaveId. Depending if the slaveId > 0 choose `sqlUpdate` or
     * `sqlInsert`
335
336
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
337
     * @param int $recordId
Carsten  Rose's avatar
Carsten Rose committed
338
     *
339
340
341
     * @return int  ACTION_ELEMENT_MODIFIED if there are potential(!) changes on the DB like INSERT / UPDATE,
     *              ACTION_ELEMENT_NO_CHANGE if nothing happened
     *              ACTION_ELEMENT_DELETED:  if a record has been deleted
Marc Egger's avatar
Marc Egger committed
342
343
344
345
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
346
     */
347
    private function doSlave(array $fe, $recordId) {
348

349
350
        $rcStatus = ACTION_ELEMENT_NO_CHANGE;

351
        // Get the slaveId
352
        $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]);
353
354
355
356
357
358
359
360
361
362
363

        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.
364
365
        $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);

366
367
        $doInsert = ($slaveId == 0);
        $doUpdate = ($slaveId != 0);
368
        $doDelete = ($slaveId != 0) && $fe[FE_SQL_DELETE] != '';
369
370
371
372
373
374
375
376
377

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

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

        if ($doUpdate) {
387
            $this->evaluate->parse($fe[FE_SQL_UPDATE]);
388
            $rcStatus = ACTION_ELEMENT_MODIFIED;
389
390
        }

391
392
393
394
        // Fire a delete query
        if ($doDelete) {
            $this->evaluate->parse($fe[FE_SQL_DELETE]);
            $slaveId = 0;
395
            $rcStatus = ACTION_ELEMENT_DELETED;
396
397
        }

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

404
        return $rcStatus;
405
    }
406

407
408
409
410
411
    /**
     * 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
412
413
414
     *
     * @return bool true if at lease one of the named elements is non empty on STORE_FORM (use SANATIZE_ALLOW_ALL to
     *              perform the check)
Marc Egger's avatar
Marc Egger committed
415
416
     * @throws \CodeException
     * @throws \UserFormException
417
418
419
420
421
422
423
424
425
426
     */
    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
427

428
429
        return false;
    }
430

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

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

        // 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;
529
            $translateMap[$newColumns[COLUMN_ID]] = $lastInsertId;
Carsten  Rose's avatar
Carsten Rose committed
530
531
532
533
534
535

            // 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 == "") {
536
                $this->doAllFormElementPaste($feSpecActionAll, $recordSourceTable, $recordDestinationTable, $field, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
537
538
539
            }
        }

540
541
542
543
544
        // 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
545
546
547
548
        return $lastInsertId;

    } // prepareDuplicate()

549
550
551
552
553
554
555
556
557
558
559
560
561
    /**
     * Translate table self referencing columns to the new values.
     * Rerun on all new records. Search and translate old id's (copied) to the new generated id's.
     *
     * Example with FormElement:  id, feIdContainer, type
     *
     *     Original:  [1,2,'input'], [2,3,'templateGroup'], [3,0, 'pill']
     *   Duplicated:  [4,2,'input'], [5,3,'templateGroup'], [6,0, 'pill']
     *  TranslateId:  [4,5,'input'], [5,6,'templateGroup'], [6,0, 'pill']
     *
     * @param array $translateMap array with old id's as keys, and new id's as their value
     * @param string $translateIdColumn column name to update. E.g. FormElement.feIdContainer, Ggroup.grId, ...
     * @param string $tableName
Marc Egger's avatar
Marc Egger committed
562
563
564
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
     */
    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
580
581
582
    /**
     * @param array $rowSrc
     * @param array $rowDest
Carsten  Rose's avatar
Carsten Rose committed
583
     *
Marc Egger's avatar
Marc Egger committed
584
     * @throws \UserFormException
Carsten  Rose's avatar
Carsten Rose committed
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
     */
    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
600
            HelperFile::mkDirParent($rowDest[$key]);
601
            HelperFile::copy($val, $rowDest[$key]);
Carsten  Rose's avatar
Carsten Rose committed
602
603
604
605
606
607
608
609
610
        }
    }

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

Carsten  Rose's avatar
Carsten Rose committed
629
            $key = $col[COLUMN_FIELD];
Carsten  Rose's avatar
Carsten Rose committed
630
            // Only copy columns which exist on source AND destination.
631
632
633
            if (!isset($row[$key])) {
                continue;
            }
Carsten  Rose's avatar
Carsten Rose committed
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
662
663
664
665
            $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()
666
}