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

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

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

23

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

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

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

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

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

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

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

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

93
        $rc = ACTION_ELEMENT_NO_CHANGE;
94

95
96
97
        // Iterate over all Action FormElements
        foreach ($feSpecAction as $fe) {

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

102
            $fe = HelperFormElement::initActionFormElement($fe);
103

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

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

124
125
            // Process templateGroup action elements
            if (isset($fe[FE_ID_CONTAINER]) && $fe[FE_ID_CONTAINER] > 0) {
126
                // Get native 'templateGroup'-FE - to retrieve MAX_LENGTH
127
                $templateGroup = $this->db->sql(SQL_FORM_ELEMENT_TEMPLATE_GROUP_FE_ID, ROW_EXPECT_1, [$fe[FE_ID_CONTAINER]],
128
                    "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');
129
130
131
132

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

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

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

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

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

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

187
            $this->sqlValidate($fe);
188

189
            if ($fe[FE_TYPE] === FE_TYPE_SENDMAIL) {
190
                $this->doSendMail($fe);
191
            }
192

193
194
195
196
197
198
199
200
201
            $rcTmp = $this->doSqlBeforeSlaveAfter($fe, $recordId, true);
            switch ($rcTmp) {
                case ACTION_ELEMENT_MODIFIED:
                case ACTION_ELEMENT_DELETED:
                    $rc = $rcTmp;
                    break;
                default:
                    break;
            }
202
        }
203

204
        return $rc;
205
206
    }

207
    /**
208
     * Process all FormElements given in the `requiredList` identified by their name.
209
210
211
212
     * If none is empty in STORE_FORM return true, else false.
     * If none FormElement is specified, return true.
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
213
     *
214
     * @return bool  true if none FE is specified or all specified are non empty.
Marc Egger's avatar
Marc Egger committed
215
216
     * @throws \CodeException
     * @throws \UserFormException
217
218
     */
    private function checkRequiredList(array $fe) {
219

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

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

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

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

235
        return true;
236
237
    }

238
239
    /**
     * @param array $feSpecAction
Marc Egger's avatar
Marc Egger committed
240
241
242
243
244
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \UserFormException
     * @throws \UserReportException
245
246
247
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
248
     */
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
    private function doSendMail(array $feSpecAction) {

        $args = array();

        $args[] = SENDMAIL_TOKEN_RECEIVER . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_TO]);
        $args[] = SENDMAIL_TOKEN_SENDER . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_FROM]);
        $args[] = SENDMAIL_TOKEN_SUBJECT . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_SUBJECT]);
        $args[] = SENDMAIL_TOKEN_BODY . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_VALUE]);
        $args[] = SENDMAIL_TOKEN_REPLY_TO . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_REPLY_TO]);
        $autoSubmit = ($this->evaluate->parse($feSpecAction[FE_SENDMAIL_FLAG_AUTO_SUBMIT]) === 'off') ? 'off' : 'on';
        $args[] = SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT . PARAM_TOKEN_DELIMITER . $autoSubmit;
        $args[] = SENDMAIL_TOKEN_GR_ID . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_GR_ID]);
        $args[] = SENDMAIL_TOKEN_X_ID . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID]);
        $args[] = SENDMAIL_TOKEN_RECEIVER_CC . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_CC]);
        $args[] = SENDMAIL_TOKEN_RECEIVER_BCC . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BCC]);
        $args[] = SENDMAIL_TOKEN_SRC . PARAM_TOKEN_DELIMITER . "FormId: " . $feSpecAction[FE_FORM_ID] . ", FormElementId: " . $feSpecAction['id'];
        $args[] = SENDMAIL_TOKEN_X_ID2 . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID2]);
        $args[] = SENDMAIL_TOKEN_X_ID3 . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_X_ID3]);
267
268
269
        $args[] = SENDMAIL_TOKEN_BODY_MODE . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BODY_MODE]);
        $args[] = SENDMAIL_TOKEN_BODY_HTML_ENTITY . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_BODY_HTML_ENTITY]);
        $args[] = SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY . PARAM_TOKEN_DELIMITER . $this->evaluate->parse($feSpecAction[FE_SENDMAIL_SUBJECT_HTML_ENTITY]);
270
        $args[] = $this->evaluate->parse($feSpecAction[FE_SENDMAIL_ATTACHMENT]??'');
271
272

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

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

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

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

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

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

313
314
315
316
317
318
        // If there is at least one record count given, who matches: return 'check succeeded'
        $countRecordsArr = explode(',', $expect);
        foreach ($countRecordsArr AS $count) {
            if (count($result) == $count) {
                return; // check succesfully passed
            }
319
320
        }

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

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

326
327
    }

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

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

349
350
        $rcStatus = ACTION_ELEMENT_NO_CHANGE;

Carsten  Rose's avatar
Carsten Rose committed
351
352
353
        if (isset($fe[FE_SLAVE_ID])) {
            // Get the slaveId
            $slaveId = $this->evaluate->parse($fe[FE_SLAVE_ID]);
354

Carsten  Rose's avatar
Carsten Rose committed
355
356
357
358
            if ($flagFeAction && $slaveId === '' && $fe[FE_NAME] !== '') {
                // If the current action element has the same name as a real master record column: take that value as an id.
                $slaveId = $this->store->getVar($fe[FE_NAME], STORE_RECORD);
            }
359

Carsten  Rose's avatar
Carsten Rose committed
360
361
362
            if ($slaveId === '' || $slaveId === false) {
                $slaveId = 0;
            }
363

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

Carsten  Rose's avatar
Carsten Rose committed
367
368
369
            $doInsert = ($slaveId == 0);
            $doUpdate = ($slaveId != 0);
            $doDelete = ($slaveId != 0) && !empty($fe[FE_SQL_DELETE]);
370

Carsten  Rose's avatar
Carsten Rose committed
371
372
373
374
375
376
            if (!empty($fe[FE_SQL_HONOR_FORM_ELEMENTS])) {
                $filled = $this->checkFormElements($fe[FE_SQL_HONOR_FORM_ELEMENTS]);
                $doInsert = $filled && $doInsert;
                $doUpdate = $filled && $doUpdate;
                $doDelete = !$filled && $doDelete;
            }
377

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

Carsten  Rose's avatar
Carsten Rose committed
386
387
388
389
            if ($doUpdate) {
                $this->evaluate->parse($fe[FE_SQL_UPDATE]);
                $rcStatus = ACTION_ELEMENT_MODIFIED;
            }
390

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

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

405
406
407
        // If given: fire a $sqlAfter query
        $this->evaluate->parse($fe[FE_SQL_AFTER]);

408
        return $rcStatus;
409
    }
410

411
412
413
414
415
    /**
     * 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
416
417
418
     *
     * @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
419
420
     * @throws \CodeException
     * @throws \UserFormException
421
422
423
424
425
426
427
428
429
430
     */
    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
431

432
433
        return false;
    }
434

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

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

        // 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;
533
            $translateMap[$newColumns[COLUMN_ID]] = $lastInsertId;
Carsten  Rose's avatar
Carsten Rose committed
534
535
536
537
538
539

            // 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 == "") {
540
                $this->doAllFormElementPaste($feSpecActionAll, $recordSourceTable, $recordDestinationTable, $field, $clipboard);
Carsten  Rose's avatar
Carsten Rose committed
541
542
543
            }
        }

544
545
546
547
548
        // 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
549
550
551
552
        return $lastInsertId;

    } // prepareDuplicate()

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

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

Carsten  Rose's avatar
Carsten Rose committed
633
            $key = $col[COLUMN_FIELD];
Carsten  Rose's avatar
Carsten Rose committed
634
            // Only copy columns which exist on source AND destination.
635
636
637
            if (!isset($row[$key])) {
                continue;
            }
Carsten  Rose's avatar
Carsten Rose committed
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
666
667
668
669
            $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()
670
}