Save.php 16.3 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
    /**
Carsten  Rose's avatar
Carsten Rose committed
102
103
     * Build an array of all values which should be saved. Values must exist as a 'form value' as well as a regular 'table column'.
     *
104
     * @param $recordId
105
     * @return int   record id (in case of insert, it's different from $recordId)
106
107
     * @throws CodeException
     * @throws DbException
108
     * @throws UserFormException
109
110
     */
    public function elements($recordId) {
111
112
        $columnCreated = false;
        $columnModified = false;
Carsten  Rose's avatar
Carsten Rose committed
113

114
115
116
        $newValues = array();

        $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES));
117
        $formValues = $this->store->getStore(STORE_FORM);
118
        $formValues = $this->createEmptyTemplateGroupElements($formValues);
119
120
121

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

123
            // Never save a predefined 'id': autoincrement values will be given by database..
124
            if ($column === COLUMN_ID) {
125
                continue;
126
            }
127

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
128
129
130
131
132
            // Skip Upload Elements: those will be processed later.
            if ($this->isColumnUploadField($column)) {
                continue;
            }

133
134
135
136
137
138
139
140
            if ($column === COLUMN_CREATED) {
                $columnCreated = true;
            }

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

141
142
            // Is there a value? Do not forget SIP values. Those do not have necessarily a FormElement.
            if (!isset($formValues[$column])) {
143
                continue;
144
145
            }

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

148
149
            Support::setIfNotSet($formValues, $column);
            $newValues[$column] = $formValues[$column];
150
151
        }

152
153
154
155
        if ($columnModified && !isset($newValues[COLUMN_MODIFIED])) {
            $newValues[COLUMN_MODIFIED] = date('YmdHis');
        }

156
        if ($recordId == 0) {
157
158
159
            if ($columnCreated && !isset($newValues[COLUMN_CREATED])) {
                $newValues[COLUMN_CREATED] = date('YmdHis');
            }
160
            $rc = $this->insertRecord($this->formSpec[F_TABLE_NAME], $newValues);
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
161

162
        } else {
163
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
164
165
166
167
            $rc = $recordId;
        }

        return $rc;
168
169
    }

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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
    /*
     * 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

221
        if ($recordId === 0) {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
222
            throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN);
223
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242

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

243
    /**
244
     * Process all Upload Formelements for the given $recordId. After processing &$formValues will be updated with the final filenames.
245
246
     *
     */
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
247
248
249
250
251
252
    public function processAllUploads($recordId) {

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

        $formValues = $this->store->getStore(STORE_FORM);
253
        $primaryRecord = $this->store->getStore(STORE_RECORD); // necessary to check if the current formElement exist as a column of the primary table.
254
255
256

        foreach ($this->feSpecNative AS $formElement) {
            // skip non upload formElements
257
            if ($formElement[FE_TYPE] != 'upload') {
258
259
260
                continue;
            }

261
262
            $formElement = HelperFormElement::initUploadFormElement($formElement);

263
264
265
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);

266
            $column = $formElement['name'];
267
            $pathFileName = $this->doUpload($formElement, $formValues[$column], $sip, $modeUpload);
268
269
270
271
272
273
274
275
276

            // 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'
277
                $this->doUploadSlave($formElement, $modeUpload);
278
279
            }
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
280

281
        // Only used in 'Simple Upload'
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
282
283
284
        if (count($newValues) > 0) {
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
        }
285
286
287
    }

    /**
288
289
290
     * 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
291
292
     * Check also: doc/CODING.md
     *
293
294
     * @param array $formElement FormElement 'upload'
     * @param string $sipUpload SIP
295
296
297
     * @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
298
299
300
301
     * @throws CodeException
     * @throws UserFormException
     * @internal param $recordId
     */
302
303
304
    private function doUpload($formElement, $sipUpload, Sip $sip, &$modeUpload) {
        $flagDelete = false;
        $modeUpload = UPLOAD_MODE_UNCHANGED;
305

306
        // Status information about upload file
307
308
309
310
311
        $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA);
        if ($statusUpload === false) {
            return false;
        }

312
313
314
315
316
317
318
        // 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);
        }

319
320
        // Delete existing old file.
        if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
321
322
            $arr = $sip->getVarsFromSip($sipUpload);
            $oldFile = $arr[EXISTING_PATH_FILE_NAME];
323
324
            if (file_exists($oldFile)) {
                if (!unlink($oldFile)) {
325
                    throw new UserFormException('Unlink file failed: ' . $oldFile, ERROR_IO_UNLINK);
326
327
                }
            }
328
329
330
331
332
333
334
335
            $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;
336
337
        }

338
339
340
341
342
343
344
345
346
347
348
        $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
349
350
351
352
     * Copy uploaded file from temporary location to final location.
     *
     * Check also: doc/CODING.md
     *
353
354
355
356
357
358
359
360
361
     * @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
362
        if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') {
363
364
365
366
            // nothing to upload: e.g. user has deleted a previous uploaded file.
            return '';
        }

367
        if (isset($formElement[FE_FILE_DESTINATION])) {
368

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

373
            $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]);
374
375
376

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

379
        if ($pathFileName === '') {
380
            throw new UserFormException("Upload failed, no target '" . FE_FILE_DESTINATION . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME);
381
382
        }

383
        if (file_exists($pathFileName)) {
384
385
386
387
388
389
390
            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);
            }
391
        }
392

393
        Support::mkDirParent($pathFileName);
394

395
396
397
398
        $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
        if (!rename($srcFile, $pathFileName)) {
            throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME);
        }
399

400
401
        return $pathFileName;
    }
402

403
    /**
404
     * Create/update or delete the slave record.
405
406
     *
     * @param array $fe
407
     * @param bool $flagNewUpload
408
409
410
411
     * @return int
     * @throws CodeException
     * @throws UserFormException
     */
412
    private function doUploadSlave(array $fe, $modeUpload) {
413
414
        $sql = '';
        $flagUpdateSlaveId = false;
415
        $flagSlaveDeleted = false;
416

417
        if (!isset($fe[FE_SLAVE_ID])) {
418
419
420
            throw new UserFormException("Missing 'slaveId'-definition", ERROR_MISSING_SLAVE_ID_DEFINITION);
        }

421
        // Get the slaveId
422
        $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
423
424
425
        // Store the slaveId: it's used and replaced in the update statement.
        $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);

426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
        $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);
447
448
        }

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

        $rc = $this->evaluate->parse($sql);
453
454
455
456
457
        // Check if the slave record has been deleted: if yes, set slaveId=0
        if ($flagSlaveDeleted && $rc > 0) {
            $rc = 0;
            $flagUpdateSlaveId = true;
        }
458
459
460
461
462
463
464
465
466
467
468
469
470

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

471
472
473
474
475
476
477
    /**
     * Get the complete FormElement for $name
     *
     * @param $name
     * @return bool|array if found the FormElement, else false.
     */
    private function getFormElementByName($name) {
478

479
480
481
482
        foreach ($this->feSpecNative as $formElement) {
            if ($formElement['name'] === $name)
                return $formElement;
        }
483

484
485
        return false;
    }
486
}