Save.php 17 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/30/16
 * Time: 7:59 PM
 */

namespace qfq;

require_once(__DIR__ . '/../qfq/store/Store.php');
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
12
require_once(__DIR__ . '/../qfq/store/Sip.php');
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/Evaluate.php');
//require_once(__DIR__ . '/../qfq/exceptions/UserException.php');
//require_once(__DIR__ . '/../qfq/exceptions/CodeException.php');
//require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
//require_once(__DIR__ . '/../qfq/Evaluate.php');


class Save {

    private $formSpec = array();  // copy of the loaded form
    private $feSpecAction = array(); // copy of all formElement.class='action' of the loaded form
    private $feSpecNative = array(); // copy of all formElement.class='native' of the loaded form
    /**
     * @var null|Store
     */
    private $store = null;
    private $db = null;

    private $evaluate = null;

    /**
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
     */
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative) {
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
        $this->store = Store::getInstance();
        $this->db = new Database();
        $this->evaluate = new Evaluate($this->store, $this->db);
    }

    /**
49
50
     * Starts save process. On succcess, returns forwardmode/page.
     *
51
     * @return int
52
53
     * @throws CodeException
     * @throws DbException
54
     * @throws UserFormException
55
56
     */
    public function process() {
57
        $rc = 0;
58
59
60
61
62

        if ($this->formSpec['multiMode'] !== 'none') {

            $parentRecords = $this->db->sql($this->formSpec['multiSql']);
            foreach ($parentRecords as $row) {
63
                $this->store->setStore($row, STORE_PARENT_RECORD, true);
64
                $rc = $this->elements($row['_id']);
65
66
            }
        } else {
67
68
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
            $rc = $this->elements($recordId);
69
        }
70
71

        return $rc;
72
73
    }

74
    /**
Carsten  Rose's avatar
Carsten Rose committed
75
76
     * Create empty FormElements based on templateGroups, for those who not already exist.
     *
77
     * @param array $formValues
Carsten  Rose's avatar
Carsten Rose committed
78
     *
79
80
81
82
83
84
     * @return array
     */
    private function createEmptyTemplateGroupElements(array $formValues) {

        foreach ($this->feSpecNative as $formElement) {

85
86
87
88
89
90
91
92
            switch ($formElement[FE_TYPE]) {
//                case FE_TYPE_EXTRA:
                case FE_TYPE_NOTE:
                case FE_TYPE_SUBRECORD:
                    continue 2;
                default:
                    break;
            }
93
94
            $feName = $formElement[FE_NAME];
            if (!isset($formValues[$feName])) {
95

96
97
98
                $formValues[$feName] = $formElement[FE_VALUE];
            }
        }
Carsten  Rose's avatar
Carsten Rose committed
99

100
101
102
        return $formValues;
    }

103
104
    /**
     * @param $feName
Carsten  Rose's avatar
Carsten Rose committed
105
     *
106
107
108
109
110
111
112
113
114
115
116
     * @return bool
     */
    private function isSetEmptyMeansNull($feName) {

        $fe = OnArray::filter($this->feSpecNative, FE_NAME, $feName);

        $flag = isset($fe[0][FE_EMPTY_MEANS_NULL]) && $fe[0][FE_EMPTY_MEANS_NULL] != '0';

        return $flag;
    }

117
    /**
Carsten  Rose's avatar
Carsten Rose committed
118
119
     * Build an array of all values which should be saved. Values must exist as a 'form value' as well as a regular
     * 'table column'.
Carsten  Rose's avatar
Carsten Rose committed
120
     *
121
     * @param $recordId
Carsten  Rose's avatar
Carsten Rose committed
122
     *
123
     * @return int   record id (in case of insert, it's different from $recordId)
124
125
     * @throws CodeException
     * @throws DbException
126
     * @throws UserFormException
127
128
     */
    public function elements($recordId) {
129
130
        $columnCreated = false;
        $columnModified = false;
Carsten  Rose's avatar
Carsten Rose committed
131

132
133
134
        $newValues = array();

        $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES));
135
        $formValues = $this->store->getStore(STORE_FORM);
136
        $formValues = $this->createEmptyTemplateGroupElements($formValues);
137
138
139

        // Iterate over all table.columns. Built an assoc array $newValues.
        foreach ($tableColumns AS $column) {
140

141
            // Never save a predefined 'id': autoincrement values will be given by database..
142
            if ($column === COLUMN_ID) {
143
                continue;
144
            }
145

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
146
147
148
149
150
            // Skip Upload Elements: those will be processed later.
            if ($this->isColumnUploadField($column)) {
                continue;
            }

151
152
153
154
155
156
157
158
            if ($column === COLUMN_CREATED) {
                $columnCreated = true;
            }

            if ($column === COLUMN_MODIFIED) {
                $columnModified = true;
            }

159
160
            // Is there a value? Do not forget SIP values. Those do not have necessarily a FormElement.
            if (!isset($formValues[$column])) {
161
                continue;
162
163
            }

164
            $this->store->setVar(SYSTEM_FORM_ELEMENT, "Column: $column", STORE_SYSTEM);
165

166
167
168
169
170
171
            // Check if an empty string has to be converted to null.
            if (isset($formValues[$column]) && $formValues[$column] == '' && $this->isSetEmptyMeansNull($column)) {
                $formValues[$column] = null;
            } else {
                Support::setIfNotSet($formValues, $column);
            }
172
            $newValues[$column] = $formValues[$column];
173

174
175
        }

176
177
178
179
        if ($columnModified && !isset($newValues[COLUMN_MODIFIED])) {
            $newValues[COLUMN_MODIFIED] = date('YmdHis');
        }

180
        if ($recordId == 0) {
181
182
183
            if ($columnCreated && !isset($newValues[COLUMN_CREATED])) {
                $newValues[COLUMN_CREATED] = date('YmdHis');
            }
184
            $rc = $this->insertRecord($this->formSpec[F_TABLE_NAME], $newValues);
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
185

186
        } else {
187
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
188
189
190
191
            $rc = $recordId;
        }

        return $rc;
192
193
    }

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
194
195
196
197
198
199
200
201
202
203
204
205
    /*
     * Checks if there is a formElement with name '$feName' of type 'upload'
     *
     * @param $feName
     * @return bool
     */
    private function isColumnUploadField($feName) {

        foreach ($this->feSpecNative AS $formElement) {
            if ($formElement[FE_NAME] === $feName && $formElement[FE_TYPE] == 'upload')
                return true;
        }
Carsten  Rose's avatar
Carsten Rose committed
206

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
207
208
209
210
211
212
213
        return false;
    }

    /**
     * Insert new record in table $this->formSpec['tableName'].
     *
     * @param array $values
Carsten  Rose's avatar
Carsten Rose committed
214
     *
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
     * @return int  last insert id
     * @throws DbException
     */
    public function insertRecord($tableName, array $values) {

        if (count($values) === 0)
            return 0; // nothing to write, last insert id=0

        $paramList = str_repeat('?, ', count($values));
        $paramList = substr($paramList, 0, strlen($paramList) - 2);
        $columnList = '`' . implode('`, `', array_keys($values)) . '`';

        $sql = 'INSERT INTO ' . $tableName . ' ( ' . $columnList . ' ) VALUES ( ' . $paramList . ' )';

        $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values));

        return $rc;
    }

    /**
     * @param string $tableName
Carsten  Rose's avatar
Carsten Rose committed
236
237
238
     * @param array  $values
     * @param int    $recordId
     *
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
239
240
241
242
243
244
245
246
247
     * @return bool|int     false if $values is empty, else affectedrows
     * @throws CodeException
     * @throws DbException
     */
    public function updateRecord($tableName, array $values, $recordId) {

        if (count($values) === 0)
            return 0; // nothing to write, 0 rows affected

248
        if ($recordId === 0) {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
249
            throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN);
250
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269

//        $paramList = str_repeat('?, ', count($values));
//        $paramList = substr($paramList, 0, strlen($paramList) - 2);

        $sql = 'UPDATE `' . $tableName . '` SET ';

        foreach ($values as $column => $value) {

            $sql .= '`' . $column . '` = ?, ';
        }

        $sql = substr($sql, 0, strlen($sql) - 2) . ' WHERE id = ?';
        $values[] = $recordId;

        $rc = $this->db->sql($sql, ROW_REGULAR, array_values($values));

        return $rc;
    }

270
    /**
Carsten  Rose's avatar
Carsten Rose committed
271
272
     * Process all Upload Formelements for the given $recordId. After processing &$formValues will be updated with the
     * final filenames.
273
274
     *
     */
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
275
276
277
278
279
280
    public function processAllUploads($recordId) {

        $sip = new Sip(false);
        $newValues = array();

        $formValues = $this->store->getStore(STORE_FORM);
281
        $primaryRecord = $this->store->getStore(STORE_RECORD); // necessary to check if the current formElement exist as a column of the primary table.
282
283
284

        foreach ($this->feSpecNative AS $formElement) {
            // skip non upload formElements
285
            if ($formElement[FE_TYPE] != 'upload') {
286
287
288
                continue;
            }

289
290
            $formElement = HelperFormElement::initUploadFormElement($formElement);

291
292
293
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);

294
            $column = $formElement['name'];
295
            $pathFileName = $this->doUpload($formElement, $formValues[$column], $sip, $modeUpload);
296
297
298
299
300
301
302
303
304

            // Upload Type: Simple or Advanced
            if (isset($primaryRecord[$column])) {
                // 'Simple Upload': no special action needed, just process the current (maybe modifired) value.
                if ($pathFileName !== false) {
                    $newValues[$column] = $pathFileName;
                }
            } else {
                // 'Advanced Upload'
305
                $this->doUploadSlave($formElement, $modeUpload);
306
307
            }
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
308

309
        // Only used in 'Simple Upload'
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
310
311
312
        if (count($newValues) > 0) {
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
        }
313
314
315
    }

    /**
316
317
318
     * Process upload for the given Formelement. If necessary, delete a previous uploaded file.
     * Calculate the final path/filename and move the file to the new location.
     *
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
319
320
     * Check also: doc/CODING.md
     *
Carsten  Rose's avatar
Carsten Rose committed
321
322
323
324
325
326
     * @param array  $formElement FormElement 'upload'
     * @param string $sipUpload   SIP
     * @param Sip    $sip
     * @param string $modeUpload  UPLOAD_MODE_UNCHANGED | UPLOAD_MODE_NEW | UPLOAD_MODE_DELETEOLD |
     *                            UPLOAD_MODE_DELETEOLD_NEW
     *
327
     * @return false|string New pathFilename or false on error
328
329
330
331
     * @throws CodeException
     * @throws UserFormException
     * @internal param $recordId
     */
332
333
334
    private function doUpload($formElement, $sipUpload, Sip $sip, &$modeUpload) {
        $flagDelete = false;
        $modeUpload = UPLOAD_MODE_UNCHANGED;
335

336
        // Status information about upload file
337
338
339
340
341
        $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA);
        if ($statusUpload === false) {
            return false;
        }

342
343
344
345
346
347
348
        // Take care the necessary target directories exist.
        $cwd = getcwd();
        $sitePath = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
        if ($cwd === false || $sitePath === false || !chdir($sitePath)) {
            throw new UserFormException("getcwd() failed or SITE_PATH undefined or chdir('$sitePath') failed.", ERROR_IO_CHDIR);
        }

349
350
        // Delete existing old file.
        if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
351
352
            $arr = $sip->getVarsFromSip($sipUpload);
            $oldFile = $arr[EXISTING_PATH_FILE_NAME];
353
354
            if (file_exists($oldFile)) {
                if (!unlink($oldFile)) {
355
                    throw new UserFormException('Unlink file failed: ' . $oldFile, ERROR_IO_UNLINK);
356
357
                }
            }
358
359
360
361
362
363
364
365
            $flagDelete = ($oldFile != '');
        }

        // Set $modeUpload
        if (isset($statusUpload[FILES_TMP_NAME]) && $statusUpload[FILES_TMP_NAME] != '') {
            $modeUpload = $flagDelete ? UPLOAD_MODE_DELETEOLD_NEW : UPLOAD_MODE_NEW;
        } else {
            $modeUpload = $flagDelete ? UPLOAD_MODE_DELETEOLD : UPLOAD_MODE_UNCHANGED;
366
367
        }

368
369
370
371
372
373
374
375
376
377
378
        $pathFileName = $this->copyUploadFile($formElement, $statusUpload);

        chdir($cwd);

        // Delete current used uniq SIP
        $this->store->setVar($sipUpload, array(), STORE_EXTRA);

        return $pathFileName;
    }

    /**
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
379
380
381
382
     * Copy uploaded file from temporary location to final location.
     *
     * Check also: doc/CODING.md
     *
383
384
     * @param array $formElement
     * @param array $statusUpload
Carsten  Rose's avatar
Carsten Rose committed
385
     *
386
387
388
389
390
391
392
     * @return array|mixed|null|string
     * @throws CodeException
     * @throws UserFormException
     */
    private function copyUploadFile(array $formElement, array $statusUpload) {
        $pathFileName = '';

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
393
        if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') {
394
395
396
397
            // nothing to upload: e.g. user has deleted a previous uploaded file.
            return '';
        }

398
        if (isset($formElement[FE_FILE_DESTINATION])) {
399

400
            // Provide variable 'filename'. Might be substituted in $formElement[FE_PATH_FILE_NAME].
401
            $origFilename = Sanitize::safeFilename($statusUpload[FILES_NAME]);
402
            $this->store->setVar(VAR_FILENAME, $origFilename, STORE_VAR);
403

404
            $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]);
405
406
407

            // Saved in store for later use during 'Advanced Upload'-post processing
            $this->store->setVar(VAR_FILE_DESTINATION, $pathFileName, STORE_VAR);
408
409
        }

410
        if ($pathFileName === '') {
411
            throw new UserFormException("Upload failed, no target '" . FE_FILE_DESTINATION . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME);
412
413
        }

414
        if (file_exists($pathFileName)) {
415
416
417
418
419
420
421
            if (isset($formElement[FE_FILE_REPLACE_MODE]) && $formElement[FE_FILE_REPLACE_MODE] == FE_FILE_REPLACE_MODE_ALWAYS) {
                if (!unlink($pathFileName)) {
                    throw new UserFormException('Copy upload failed - file exist and unlink() failed: ' . $pathFileName, ERROR_IO_UNLINK);
                }
            } else {
                throw new UserFormException('Copy upload failed - file already exist: ' . $pathFileName, ERROR_IO_FILE_EXIST);
            }
422
        }
423

424
        Support::mkDirParent($pathFileName);
425

426
427
428
429
        $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
        if (!rename($srcFile, $pathFileName)) {
            throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME);
        }
430

431
432
        return $pathFileName;
    }
433

434
    /**
435
     * Create/update or delete the slave record.
436
437
     *
     * @param array $fe
Carsten  Rose's avatar
Carsten Rose committed
438
439
     * @param bool  $flagNewUpload
     *
440
441
442
443
     * @return int
     * @throws CodeException
     * @throws UserFormException
     */
444
    private function doUploadSlave(array $fe, $modeUpload) {
445
446
        $sql = '';
        $flagUpdateSlaveId = false;
447
        $flagSlaveDeleted = false;
448

449
        if (!isset($fe[FE_SLAVE_ID])) {
450
451
452
            throw new UserFormException("Missing 'slaveId'-definition", ERROR_MISSING_SLAVE_ID_DEFINITION);
        }

453
        // Get the slaveId
454
        $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
455
456
457
        // Store the slaveId: it's used and replaced in the update statement.
        $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);

458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
        $mode = ($slaveId == '0') ? 'I' : 'U'; // I=Insert, U=Update
        $mode .= ($modeUpload == UPLOAD_MODE_NEW || $modeUpload == UPLOAD_MODE_DELETEOLD_NEW) ? 'N' : ''; // N=New File, '' if no new file.
        $mode .= ($modeUpload == UPLOAD_MODE_DELETEOLD) ? 'D' : ''; // Delete slave record only if there is no new and not 'unchanged'.
        switch ($mode) {
            case 'IN':
                $sql = $fe[FE_SQL_INSERT];
                $flagUpdateSlaveId = true;
                break;
            case 'UN':
                $sql = $fe[FE_SQL_UPDATE];
                break;
            case 'I':
            case 'U':
                $sql = ''; // no old file and no new file.
                break;
            case 'UD':
                $sql = $fe[FE_SQL_DELETE];
                $flagSlaveDeleted = true;
                break;
            default:
                throw new CodeException('Unknown mode: ' . $mode, ERROR_UNKNOWN_MODE);
479
480
        }

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

        $rc = $this->evaluate->parse($sql);
485
486
487
488
489
        // Check if the slave record has been deleted: if yes, set slaveId=0
        if ($flagSlaveDeleted && $rc > 0) {
            $rc = 0;
            $flagUpdateSlaveId = true;
        }
490
491
492
493
494
495
496
497
498
499
500
501
502

        if ($flagUpdateSlaveId) {
            // Store the slaveId: it's used and replaced in the update statement.
            $this->store->setVar(VAR_SLAVE_ID, $rc, STORE_VAR, true);
            $slaveId = $rc;
        }

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

        return $slaveId;
    }

503
504
505
506
    /**
     * Get the complete FormElement for $name
     *
     * @param $name
Carsten  Rose's avatar
Carsten Rose committed
507
     *
508
509
510
     * @return bool|array if found the FormElement, else false.
     */
    private function getFormElementByName($name) {
511

512
513
514
515
        foreach ($this->feSpecNative as $formElement) {
            if ($formElement['name'] === $name)
                return $formElement;
        }
516

517
518
        return false;
    }
519
}