Save.php 16.1 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
84
85
86
87
88
89
90
91
     * @param array $formValues
     * @return array
     */
    private function createEmptyTemplateGroupElements(array $formValues) {

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

            $feName = $formElement[FE_NAME];
            if (!isset($formValues[$feName])) {
                $formValues[$feName] = $formElement[FE_VALUE];
            }
        }
        return $formValues;
    }

92
    /**
Carsten  Rose's avatar
Carsten Rose committed
93
94
     * Build an array of all values which should be saved. Values must exist as a 'form value' as well as a regular 'table column'.
     *
95
     * @param $recordId
96
     * @return int   record id (in case of insert, it's different from $recordId)
97
98
     * @throws CodeException
     * @throws DbException
99
     * @throws UserFormException
100
101
     */
    public function elements($recordId) {
102
103
        $columnCreated = false;
        $columnModified = false;
Carsten  Rose's avatar
Carsten Rose committed
104

105
106
107
        $newValues = array();

        $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES));
108
        $formValues = $this->store->getStore(STORE_FORM);
109
        $formValues = $this->createEmptyTemplateGroupElements($formValues);
110
111
112

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

114
            // Never save a predefined 'id': autoincrement values will be given by database..
115
            if ($column === 'id') {
116
                continue;
117
            }
118

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
119
120
121
122
123
            // Skip Upload Elements: those will be processed later.
            if ($this->isColumnUploadField($column)) {
                continue;
            }

124
125
126
127
128
129
130
131
            if ($column === COLUMN_CREATED) {
                $columnCreated = true;
            }

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

132
133
            // Is there a value? Do not forget SIP values. Those do not have necessarily a FormElement.
            if (!isset($formValues[$column])) {
134
                continue;
135
136
            }

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

139
140
            Support::setIfNotSet($formValues, $column);
            $newValues[$column] = $formValues[$column];
141
142
        }

143
144
145
146
        if ($columnModified && !isset($newValues[COLUMN_MODIFIED])) {
            $newValues[COLUMN_MODIFIED] = date('YmdHis');
        }

147
        if ($recordId == 0) {
148
149
150
            if ($columnCreated && !isset($newValues[COLUMN_CREATED])) {
                $newValues[COLUMN_CREATED] = date('YmdHis');
            }
151
            $rc = $this->insertRecord($this->formSpec[F_TABLE_NAME], $newValues);
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
152

153
        } else {
154
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
155
156
157
158
            $rc = $recordId;
        }

        return $rc;
159
160
    }

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
161
162
163
164
165
166
167
168
169
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
    /*
     * 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

212
        if ($recordId === 0) {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
213
            throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN);
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

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

234
    /**
235
     * Process all Upload Formelements for the given $recordId. After processing &$formValues will be updated with the final filenames.
236
237
     *
     */
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
238
239
240
241
242
243
    public function processAllUploads($recordId) {

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

        $formValues = $this->store->getStore(STORE_FORM);
244
        $primaryRecord = $this->store->getStore(STORE_RECORD); // necessary to check if the current formElement exist as a column of the primary table.
245
246
247

        foreach ($this->feSpecNative AS $formElement) {
            // skip non upload formElements
248
            if ($formElement[FE_TYPE] != 'upload') {
249
250
251
                continue;
            }

252
253
            $formElement = HelperFormElement::initUploadFormElement($formElement);

254
255
256
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);

257
            $column = $formElement['name'];
258
            $pathFileName = $this->doUpload($formElement, $formValues[$column], $sip, $modeUpload);
259
260
261
262
263
264
265
266
267

            // 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'
268
                $this->doUploadSlave($formElement, $modeUpload);
269
270
            }
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
271

272
        // Only used in 'Simple Upload'
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
273
274
275
        if (count($newValues) > 0) {
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
        }
276
277
278
    }

    /**
279
280
281
     * 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
282
283
     * Check also: doc/CODING.md
     *
284
285
     * @param array $formElement FormElement 'upload'
     * @param string $sipUpload SIP
286
287
288
     * @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
289
290
291
292
     * @throws CodeException
     * @throws UserFormException
     * @internal param $recordId
     */
293
294
295
    private function doUpload($formElement, $sipUpload, Sip $sip, &$modeUpload) {
        $flagDelete = false;
        $modeUpload = UPLOAD_MODE_UNCHANGED;
296

297
        // Status information about upload file
298
299
300
301
302
        $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA);
        if ($statusUpload === false) {
            return false;
        }

303
304
305
306
307
308
309
        // 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);
        }

310
311
        // Delete existing old file.
        if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
312
313
            $arr = $sip->getVarsFromSip($sipUpload);
            $oldFile = $arr[EXISTING_PATH_FILE_NAME];
314
315
            if (file_exists($oldFile)) {
                if (!unlink($oldFile)) {
316
                    throw new UserFormException('Unlink file failed: ' . $oldFile, ERROR_IO_UNLINK);
317
318
                }
            }
319
320
321
322
323
324
325
326
            $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;
327
328
        }

329
330
331
332
333
334
335
336
337
338
339
        $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
340
341
342
343
     * Copy uploaded file from temporary location to final location.
     *
     * Check also: doc/CODING.md
     *
344
345
346
347
348
349
350
351
352
     * @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
353
        if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') {
354
355
356
357
            // nothing to upload: e.g. user has deleted a previous uploaded file.
            return '';
        }

358
        if (isset($formElement[FE_FILE_DESTINATION])) {
359

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

364
            $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]);
365
366
367

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

370
        if ($pathFileName === '') {
371
            throw new UserFormException("Upload failed, no target '" . FE_FILE_DESTINATION . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME);
372
373
        }

374
        if (file_exists($pathFileName)) {
375
376
377
378
379
380
381
            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);
            }
382
        }
383

384
        Support::mkDirParent($pathFileName);
385

386
387
388
389
        $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
        if (!rename($srcFile, $pathFileName)) {
            throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME);
        }
390

391
392
        return $pathFileName;
    }
393

394
    /**
395
     * Create/update or delete the slave record.
396
397
     *
     * @param array $fe
398
     * @param bool $flagNewUpload
399
400
401
402
     * @return int
     * @throws CodeException
     * @throws UserFormException
     */
403
    private function doUploadSlave(array $fe, $modeUpload) {
404
405
        $sql = '';
        $flagUpdateSlaveId = false;
406
        $flagSlaveDeleted = false;
407

408
        if (!isset($fe[FE_SLAVE_ID])) {
409
410
411
            throw new UserFormException("Missing 'slaveId'-definition", ERROR_MISSING_SLAVE_ID_DEFINITION);
        }

412
        // Get the slaveId
413
        $slaveId = Support::falseEmptyToZero($this->evaluate->parse($fe[FE_SLAVE_ID]));
414
415
416
        // Store the slaveId: it's used and replaced in the update statement.
        $this->store->setVar(VAR_SLAVE_ID, $slaveId, STORE_VAR, true);

417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
        $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);
438
439
        }

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

        $rc = $this->evaluate->parse($sql);
444
445
446
447
448
        // Check if the slave record has been deleted: if yes, set slaveId=0
        if ($flagSlaveDeleted && $rc > 0) {
            $rc = 0;
            $flagUpdateSlaveId = true;
        }
449
450
451
452
453
454
455
456
457
458
459
460
461

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

462
463
464
465
466
467
468
    /**
     * Get the complete FormElement for $name
     *
     * @param $name
     * @return bool|array if found the FormElement, else false.
     */
    private function getFormElementByName($name) {
469

470
471
472
473
        foreach ($this->feSpecNative as $formElement) {
            if ($formElement['name'] === $name)
                return $formElement;
        }
474

475
476
477
        return false;
    }

478
}