Save.php 15.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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
    /**
     * @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;
    }

90
91
    /**
     * @param $recordId
92
     * @return int   record id (in case of insert, it's different from $recordId)
93
94
     * @throws CodeException
     * @throws DbException
95
     * @throws UserFormException
96
97
     */
    public function elements($recordId) {
98
99
        $columnCreated = false;
        $columnModified = false;
Carsten  Rose's avatar
Carsten Rose committed
100

101
102
103
        $newValues = array();

        $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES));
104
        $formValues = $this->store->getStore(STORE_FORM);
105
        $formValues = $this->createEmptyTemplateGroupElements($formValues);
106
107
108

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

110
            // Never save a predefined 'id': autoincrement values will be given by database..
111
            if ($column === 'id') {
112
                continue;
113
            }
114

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
115
116
117
118
119
            // Skip Upload Elements: those will be processed later.
            if ($this->isColumnUploadField($column)) {
                continue;
            }

120
121
122
123
124
125
126
127
            if ($column === COLUMN_CREATED) {
                $columnCreated = true;
            }

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

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

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

135
136
            Support::setIfNotSet($formValues, $column);
            $newValues[$column] = $formValues[$column];
137
138
        }

139
140
141
142
        if ($columnModified && !isset($newValues[COLUMN_MODIFIED])) {
            $newValues[COLUMN_MODIFIED] = date('YmdHis');
        }

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

149
        } else {
150
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId);
151
152
153
154
            $rc = $recordId;
        }

        return $rc;
155
156
    }

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
157
158
159
160
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
    /*
     * 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

208
        if ($recordId === 0) {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
209
            throw new CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN);
210
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229

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

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

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

        $formValues = $this->store->getStore(STORE_FORM);
240
        $primaryRecord = $this->store->getStore(STORE_RECORD); // necessary to check if the current formElement exist as a column of the primary table.
241
242
243

        foreach ($this->feSpecNative AS $formElement) {
            // skip non upload formElements
244
            if ($formElement[FE_TYPE] != 'upload') {
245
246
247
                continue;
            }

248
249
            $formElement = HelperFormElement::initUploadFormElement($formElement);

250
251
252
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);

253
            $column = $formElement['name'];
254
            $pathFileName = $this->doUpload($formElement, $formValues[$column], $sip, $modeUpload);
255
256
257
258
259
260
261
262
263

            // 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'
264
                $this->doUploadSlave($formElement, $modeUpload);
265
266
            }
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
267

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

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

293
        // Status information about upload file
294
295
296
297
298
        $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA);
        if ($statusUpload === false) {
            return false;
        }

299
300
301
302
303
304
305
        // 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);
        }

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

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

354
        if (isset($formElement[FE_FILE_DESTINATION])) {
355

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

360
            $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]);
361
362
363

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

366
        if ($pathFileName === '') {
367
            throw new UserFormException("Upload failed, no target '" . FE_FILE_DESTINATION . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME);
368
369
        }

370
        if (file_exists($pathFileName)) {
371
372
373
374
375
376
377
            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);
            }
378
        }
379

380
        Support::mkDirParent($pathFileName);
381

382
383
384
385
        $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
        if (!rename($srcFile, $pathFileName)) {
            throw new UserFormException("Rename file: '$srcFile' > '$pathFileName'", ERROR_IO_RENAME);
        }
386

387
388
        return $pathFileName;
    }
389

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

404
        if (!isset($fe[FE_SLAVE_ID])) {
405
406
407
            throw new UserFormException("Missing 'slaveId'-definition", ERROR_MISSING_SLAVE_ID_DEFINITION);
        }

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

413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
        $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);
434
435
        }

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

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

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

458
459
460
461
462
463
464
    /**
     * Get the complete FormElement for $name
     *
     * @param $name
     * @return bool|array if found the FormElement, else false.
     */
    private function getFormElementByName($name) {
465

466
467
468
469
        foreach ($this->feSpecNative as $formElement) {
            if ($formElement['name'] === $name)
                return $formElement;
        }
470

471
472
473
        return false;
    }

474
}