Save.php 16.9 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
78
79
80
81
82
83
     * @param array $formValues
     * @return array
     */
    private function createEmptyTemplateGroupElements(array $formValues) {

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

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

95
96
97
98
99
100
                $formValues[$feName] = $formElement[FE_VALUE];
            }
        }
        return $formValues;
    }

101
102
103
104
105
106
107
108
109
110
111
112
113
    /**
     * @param $feName
     * @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;
    }

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

127
128
129
        $newValues = array();

        $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES));
130
        $formValues = $this->store->getStore(STORE_FORM);
131
        $formValues = $this->createEmptyTemplateGroupElements($formValues);
132
133
134

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

136
            // Never save a predefined 'id': autoincrement values will be given by database..
137
            if ($column === COLUMN_ID) {
138
                continue;
139
            }
140

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
141
142
143
144
145
            // Skip Upload Elements: those will be processed later.
            if ($this->isColumnUploadField($column)) {
                continue;
            }

146
147
148
149
150
151
152
153
            if ($column === COLUMN_CREATED) {
                $columnCreated = true;
            }

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

154
155
            // Is there a value? Do not forget SIP values. Those do not have necessarily a FormElement.
            if (!isset($formValues[$column])) {
156
                continue;
157
158
            }

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

161
162
163
164
165
166
            // 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);
            }
167
            $newValues[$column] = $formValues[$column];
168

169
170
        }

171
172
173
174
        if ($columnModified && !isset($newValues[COLUMN_MODIFIED])) {
            $newValues[COLUMN_MODIFIED] = date('YmdHis');
        }

175
        if ($recordId == 0) {
176
177
178
            if ($columnCreated && !isset($newValues[COLUMN_CREATED])) {
                $newValues[COLUMN_CREATED] = date('YmdHis');
            }
179
            $rc = $this->insertRecord($this->formSpec[F_TABLE_NAME], $newValues);
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
180

181
        } else {
182
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
183
184
185
186
            $rc = $recordId;
        }

        return $rc;
187
188
    }

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
    /*
     * 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;
        }
        return false;
    }

    /**
     * Insert new record in table $this->formSpec['tableName'].
     *
     * @param array $values
     * @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
     * @param array $values
     * @param int $recordId
     * @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

240
        if ($recordId === 0) {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
241
            throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN);
242
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261

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

262
    /**
263
     * Process all Upload Formelements for the given $recordId. After processing &$formValues will be updated with the final filenames.
264
265
     *
     */
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
266
267
268
269
270
271
    public function processAllUploads($recordId) {

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

        $formValues = $this->store->getStore(STORE_FORM);
272
        $primaryRecord = $this->store->getStore(STORE_RECORD); // necessary to check if the current formElement exist as a column of the primary table.
273
274
275

        foreach ($this->feSpecNative AS $formElement) {
            // skip non upload formElements
276
            if ($formElement[FE_TYPE] != 'upload') {
277
278
279
                continue;
            }

280
281
            $formElement = HelperFormElement::initUploadFormElement($formElement);

282
283
284
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);

285
            $column = $formElement['name'];
286
            $pathFileName = $this->doUpload($formElement, $formValues[$column], $sip, $modeUpload);
287
288
289
290
291
292
293
294
295

            // 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'
296
                $this->doUploadSlave($formElement, $modeUpload);
297
298
            }
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
299

300
        // Only used in 'Simple Upload'
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
301
302
303
        if (count($newValues) > 0) {
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
        }
304
305
306
    }

    /**
307
308
309
     * 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
310
311
     * Check also: doc/CODING.md
     *
312
313
     * @param array $formElement FormElement 'upload'
     * @param string $sipUpload SIP
314
315
316
     * @param Sip $sip
     * @param string $modeUpload UPLOAD_MODE_UNCHANGED | UPLOAD_MODE_NEW | UPLOAD_MODE_DELETEOLD | UPLOAD_MODE_DELETEOLD_NEW
     * @return false|string New pathFilename or false on error
317
318
319
320
     * @throws CodeException
     * @throws UserFormException
     * @internal param $recordId
     */
321
322
323
    private function doUpload($formElement, $sipUpload, Sip $sip, &$modeUpload) {
        $flagDelete = false;
        $modeUpload = UPLOAD_MODE_UNCHANGED;
324

325
        // Status information about upload file
326
327
328
329
330
        $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA);
        if ($statusUpload === false) {
            return false;
        }

331
332
333
334
335
336
337
        // 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);
        }

338
339
        // Delete existing old file.
        if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
340
341
            $arr = $sip->getVarsFromSip($sipUpload);
            $oldFile = $arr[EXISTING_PATH_FILE_NAME];
342
343
            if (file_exists($oldFile)) {
                if (!unlink($oldFile)) {
344
                    throw new UserFormException('Unlink file failed: ' . $oldFile, ERROR_IO_UNLINK);
345
346
                }
            }
347
348
349
350
351
352
353
354
            $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;
355
356
        }

357
358
359
360
361
362
363
364
365
366
367
        $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
368
369
370
371
     * Copy uploaded file from temporary location to final location.
     *
     * Check also: doc/CODING.md
     *
372
373
374
375
376
377
378
379
380
     * @param array $formElement
     * @param array $statusUpload
     * @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
381
        if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') {
382
383
384
385
            // nothing to upload: e.g. user has deleted a previous uploaded file.
            return '';
        }

386
        if (isset($formElement[FE_FILE_DESTINATION])) {
387

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

392
            $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]);
393
394
395

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

398
        if ($pathFileName === '') {
399
            throw new UserFormException("Upload failed, no target '" . FE_FILE_DESTINATION . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME);
400
401
        }

402
        if (file_exists($pathFileName)) {
403
404
405
406
407
408
409
            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);
            }
410
        }
411

412
        Support::mkDirParent($pathFileName);
413

414
415
416
417
        $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
        if (!rename($srcFile, $pathFileName)) {
            throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME);
        }
418

419
420
        return $pathFileName;
    }
421

422
    /**
423
     * Create/update or delete the slave record.
424
425
     *
     * @param array $fe
426
     * @param bool $flagNewUpload
427
428
429
430
     * @return int
     * @throws CodeException
     * @throws UserFormException
     */
431
    private function doUploadSlave(array $fe, $modeUpload) {
432
433
        $sql = '';
        $flagUpdateSlaveId = false;
434
        $flagSlaveDeleted = false;
435

436
        if (!isset($fe[FE_SLAVE_ID])) {
437
438
439
            throw new UserFormException("Missing 'slaveId'-definition", ERROR_MISSING_SLAVE_ID_DEFINITION);
        }

440
        // Get the slaveId
441
        $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
442
443
444
        // Store the slaveId: it's used and replaced in the update statement.
        $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);

445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
        $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);
466
467
        }

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

        $rc = $this->evaluate->parse($sql);
472
473
474
475
476
        // Check if the slave record has been deleted: if yes, set slaveId=0
        if ($flagSlaveDeleted && $rc > 0) {
            $rc = 0;
            $flagUpdateSlaveId = true;
        }
477
478
479
480
481
482
483
484
485
486
487
488
489

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

490
491
492
493
494
495
496
    /**
     * Get the complete FormElement for $name
     *
     * @param $name
     * @return bool|array if found the FormElement, else false.
     */
    private function getFormElementByName($name) {
497

498
499
500
501
        foreach ($this->feSpecNative as $formElement) {
            if ($formElement['name'] === $name)
                return $formElement;
        }
502

503
504
        return false;
    }
505
}