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

Marc Egger's avatar
Marc Egger committed
9
10
11
namespace IMATHUZH\Qfq\Core;

use IMATHUZH\Qfq\Core\Database\Database;
12
use IMATHUZH\Qfq\Core\Helper\HelperFile;
Marc Egger's avatar
Marc Egger committed
13
14
use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
use IMATHUZH\Qfq\Core\Helper\Logger;
15
use IMATHUZH\Qfq\Core\Helper\OnArray;
Marc Egger's avatar
Marc Egger committed
16
17
18
use IMATHUZH\Qfq\Core\Helper\Sanitize;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Store\Sip;
19
20
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Store\FillStoreForm;
21

22
23
24
25
/**
 * Class Save
 * @package qfq
 */
26
27
28
29
30
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
31
    private $feSpecNativeRaw = array(); // copy of all formElement.class='native' of the loaded form
32
33
34
35
36
37
38
39
    /**
     * @var null|Store
     */
    private $store = null;
    private $db = null;

    private $evaluate = null;

40
41
    private $qfqLogFilename = '';

42
43
44
45
    /**
     * @param array $formSpec
     * @param array $feSpecAction
     * @param array $feSpecNative
46
     * @param array $feSpecNativeRaw
Marc Egger's avatar
Marc Egger committed
47
48
49
50
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
51
     */
52
    public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $feSpecNativeRaw) {
53

54
55
56
        $this->formSpec = $formSpec;
        $this->feSpecAction = $feSpecAction;
        $this->feSpecNative = $feSpecNative;
57
        $this->feSpecNativeRaw = $feSpecNativeRaw;
58
        $this->store = Store::getInstance();
59
        $this->db = new Database($formSpec[F_DB_INDEX]);
60
        $this->evaluate = new Evaluate($this->store, $this->db);
61
62
63

        $this->qfqLogFilename = $this->store->getVar(SYSTEM_QFQ_LOG, STORE_SYSTEM);

64
65
66
    }

    /**
Carsten  Rose's avatar
Carsten Rose committed
67
     * Starts save process. Returns recordId.
68
     *
69
     * @return int
Marc Egger's avatar
Marc Egger committed
70
71
72
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
73
     * @throws \UserReportException
74
75
     */
    public function process() {
76
        $rc = 0;
77

78
        if ($this->formSpec[F_MULTI_SQL] !== '') {
79
            $rc = $this->saveMultiForm();
80
        } else {
81
82
            $recordId = $this->store->getVar(SIP_RECORD_ID, STORE_SIP . STORE_ZERO);
            $rc = $this->elements($recordId);
83
        }
84
85

        return $rc;
86
87
    }

88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
    /**
     * @return int|string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    private function saveMultiForm() {

        $parentRecords = $this->evaluate->parse($this->formSpec[F_MULTI_SQL], ROW_REGULAR);

        // No rows: nothing to do.
        if (empty($parentRecords)) {
            //TODO Meldung konfigurierbar machen.
            return 'No data';
        }

        // Check for 'id' or '_id' as column name
        $idName = isset($parentRecords[0]['_' . F_MULTI_COL_ID]) ? '_' . F_MULTI_COL_ID : F_MULTI_COL_ID;

        // Check that an column 'id' is given
        if (!isset($parentRecords[0][$idName])) {
            throw new \UserFormException(
                json_encode([ERROR_MESSAGE_TO_USER => 'Missing column "_' . F_MULTI_COL_ID . '"', ERROR_MESSAGE_TO_DEVELOPER => $this->formSpec[F_MULTI_SQL]]),
                ERROR_INVALID_OR_MISSING_PARAMETER);
        }

        $fillStoreForm = new FillStoreForm();

        foreach ($parentRecords as $row) {
            $this->store->setStore($row, STORE_PARENT_RECORD, true);

            // Fake current recordId
            $this->store->setVar(SIP_RECORD_ID, $row[$idName], STORE_SIP);
            $fillStoreForm->process(FORM_SAVE);

            $rc = $this->elements($row[$idName]);
        }

        return $rc;
    }

130
    /**
Carsten  Rose's avatar
Carsten Rose committed
131
132
     * Create empty FormElements based on templateGroups, for those who not already exist.
     *
133
     * @param array $formValues
Carsten  Rose's avatar
Carsten Rose committed
134
     *
135
     * @return array
Marc Egger's avatar
Marc Egger committed
136
     * @throws \UserFormException
137
138
139
140
141
     */
    private function createEmptyTemplateGroupElements(array $formValues) {

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

142
143
144
145
146
147
148
149
            switch ($formElement[FE_TYPE]) {
//                case FE_TYPE_EXTRA:
                case FE_TYPE_NOTE:
                case FE_TYPE_SUBRECORD:
                    continue 2;
                default:
                    break;
            }
150
            $feName = $formElement[FE_NAME];
151
            if (!isset($formValues[$feName]) && $this->isMemberOfTemplateGroup($formElement)) {
152
153
154
                $formValues[$feName] = $formElement[FE_VALUE];
            }
        }
Carsten  Rose's avatar
Carsten Rose committed
155

156
157
158
        return $formValues;
    }

159
    /**
160
161
162
163
164
     * Check if the current $formElement is member of a templateGroup.
     *
     * @param array $formElement
     * @param int $depth
     * @return bool
Marc Egger's avatar
Marc Egger committed
165
     * @throws \UserFormException
166
167
168
169
170
     */
    private function isMemberOfTemplateGroup(array $formElement, $depth = 0) {
        $depth++;

        if ($depth > 15) {
Marc Egger's avatar
Marc Egger committed
171
            throw new \UserFormException('FormElement nested too much (in each other - endless?): stop recursion', ERROR_FE_NESTED_TOO_MUCH);
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
        }

        if ($formElement[FE_TYPE] == FE_TYPE_TEMPLATE_GROUP) {
            return true;
        }

        if ($formElement[FE_ID_CONTAINER] == 0) {
            return false;
        }

        // Get the parent element
        $formElementArr = OnArray::filter($this->feSpecNativeRaw, FE_ID, $formElement[FE_ID_CONTAINER]);
        if (isset($formElementArr[0])) {
            return $this->isMemberOfTemplateGroup($formElementArr[0], $depth);
        }

        return false; // This should not be reached,
    }

    /**
     *
193
     * @param $feName
Carsten  Rose's avatar
Carsten Rose committed
194
     *
195
196
197
198
199
200
201
202
203
204
205
     * @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;
    }

206
    /**
Carsten  Rose's avatar
Carsten Rose committed
207
208
     * 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
209
     *
210
     * @param $recordId
Carsten  Rose's avatar
Carsten Rose committed
211
     *
212
     * @return int   record id (in case of insert, it's different from $recordId)
Marc Egger's avatar
Marc Egger committed
213
214
215
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \DbException
216
     */
217
    private function elements($recordId) {
218
219
        $columnCreated = false;
        $columnModified = false;
Carsten  Rose's avatar
Carsten Rose committed
220

221
222
223
        $newValues = array();

        $tableColumns = array_keys($this->store->getStore(STORE_TABLE_COLUMN_TYPES));
224
        $formValues = $this->store->getStore(STORE_FORM);
225
        $formValues = $this->createEmptyTemplateGroupElements($formValues);
226
227
228

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

230
            // Never save a predefined 'id': autoincrement values will be given by database..
231
            if ($column === COLUMN_ID) {
232
                continue;
233
            }
234

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
235
236
237
238
239
            // Skip Upload Elements: those will be processed later.
            if ($this->isColumnUploadField($column)) {
                continue;
            }

240
241
242
243
244
245
246
247
            if ($column === COLUMN_CREATED) {
                $columnCreated = true;
            }

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

248
249
            // Is there a value? Do not forget SIP values. Those do not have necessarily a FormElement.
            if (!isset($formValues[$column])) {
250
                continue;
251
252
            }

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

255
256
257
258
259
260
            // 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);
            }
261
            $newValues[$column] = $formValues[$column];
262

263
264
        }

265
266
267
268
        if ($columnModified && !isset($newValues[COLUMN_MODIFIED])) {
            $newValues[COLUMN_MODIFIED] = date('YmdHis');
        }

269
        if ($recordId == 0) {
270
271
272
            if ($columnCreated && !isset($newValues[COLUMN_CREATED])) {
                $newValues[COLUMN_CREATED] = date('YmdHis');
            }
273
            $rc = $this->insertRecord($this->formSpec[F_TABLE_NAME], $newValues);
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
274

275
        } else {
276
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId, $this->formSpec[F_PRIMARY_KEY]);
277
278
279
280
            $rc = $recordId;
        }

        return $rc;
281
282
    }

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
283
284
285
286
287
288
    /*
     * Checks if there is a formElement with name '$feName' of type 'upload'
     *
     * @param $feName
     * @return bool
     */
289
290
291
292
    /**
     * @param $feName
     * @return bool
     */
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
293
294
295
296
297
298
    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
299

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
300
301
302
303
304
305
        return false;
    }

    /**
     * Insert new record in table $this->formSpec['tableName'].
     *
306
     * @param $tableName
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
307
     * @param array $values
Carsten  Rose's avatar
Carsten Rose committed
308
     *
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
309
     * @return int  last insert id
Marc Egger's avatar
Marc Egger committed
310
311
312
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
313
314
315
316
317
318
319
320
321
322
     */
    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)) . '`';

323
        $sql = "INSERT INTO $tableName ( " . $columnList . " ) VALUES ( " . $paramList . ' )';
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
324
325
326
327
328
329
330
331

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

        return $rc;
    }

    /**
     * @param string $tableName
Carsten  Rose's avatar
Carsten Rose committed
332
333
     * @param array $values
     * @param int $recordId
334
     * @param string $primaryKey
Carsten  Rose's avatar
Carsten Rose committed
335
     *
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
336
     * @return bool|int     false if $values is empty, else affectedrows
Marc Egger's avatar
Marc Egger committed
337
338
339
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
340
     */
341
    public function updateRecord($tableName, array $values, $recordId, $primaryKey = F_PRIMARY_KEY_DEFAULT) {
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
342
343
344
345

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

346
        if ($recordId === 0) {
Marc Egger's avatar
Marc Egger committed
347
            throw new \CodeException('RecordId=0 - this is not possible for update.', ERROR_RECORDID_0_FORBIDDEN);
348
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
349
350
351
352
353
354
355
356

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

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

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

357
        $sql = substr($sql, 0, strlen($sql) - 2) . " WHERE $primaryKey = ?";
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
358
359
360
361
362
363
364
        $values[] = $recordId;

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

        return $rc;
    }

365
    /**
Carsten  Rose's avatar
Carsten Rose committed
366
367
     * Process all Upload Formelements for the given $recordId. After processing &$formValues will be updated with the
     * final filenames.
368
369
370
371
372
373
374
375
     *
     * Constellation: # FILE OLD   FILE NEW     FILESIZE
     *                1 none       none
     *                2 none       new
     *                3 exist      no change
     *                4 delete     none
     *                5 delete     new
     *
376
     * @param $recordId
Marc Egger's avatar
Marc Egger committed
377
378
379
380
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
381
382
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
383
     */
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
384
385
386
387
    public function processAllUploads($recordId) {

        $sip = new Sip(false);
        $newValues = array();
388
        $vars = array();
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
389
390

        $formValues = $this->store->getStore(STORE_FORM);
391
        $primaryRecord = $this->store->getStore(STORE_RECORD); // necessary to check if the current formElement exist as a column of the primary table.
392
393
394

        foreach ($this->feSpecNative AS $formElement) {
            // skip non upload formElements
395
            if ($formElement[FE_TYPE] != FE_TYPE_UPLOAD) {
396
397
398
                continue;
            }

399
400
401
402
            // Preparation for Log, Debug
            $this->store->setVar(SYSTEM_FORM_ELEMENT, Logger::formatFormElementName($formElement), STORE_SYSTEM);
            $this->store->setVar(SYSTEM_FORM_ELEMENT_ID, $formElement[FE_ID], STORE_SYSTEM);

403
            $formElement = HelperFormElement::initUploadFormElement($formElement);
404
            if (isset($formElement[FE_FILL_STORE_VAR])) {
405
                $formElement[FE_FILL_STORE_VAR] = $this->evaluate->parse($formElement[FE_FILL_STORE_VAR], ROW_EXPECT_0_1);
406
                $this->store->appendToStore($formElement[FE_FILL_STORE_VAR], STORE_VAR);
407
            }
408

409
            $column = $formElement[FE_NAME];
410
            $pathFileName = $this->doUpload($formElement, ($formValues[$column] ?? ''), $sip, $modeUpload);
411

412
413
            if ($modeUpload == UPLOAD_MODE_DELETEOLD && $pathFileName == '') {
                $pathFileNameTmp = '';  // see '4'
414
415
            } else {
                if (empty($pathFileName)) {
416
                    $pathFileNameTmp = $primaryRecord[$column] ?? ''; // see '3'. Attention: in case of Advanced Upload, $primaryRecord[$column] does not exist.
417
418
419
420
421
                } else {
                    $pathFileNameTmp = $pathFileName; // see '1,2,5'
                }
            }

422
            // Get latest file information
423
            if ($pathFileNameTmp == '') {
424
425
426
                // No new upload and no existing: take care to remove previous upload file statistics.
                $this->store->unsetVar(VAR_FILE_MIME_TYPE, STORE_VAR);
                $this->store->unsetVar(VAR_FILE_SIZE, STORE_VAR);
427
428
                $vars[VAR_FILE_SIZE] = 0;
                $vars[VAR_FILE_MIME_TYPE] = '';
429
430
431
432
433
            } else {
                $vars = HelperFile::getFileStat($pathFileNameTmp);
                $this->store->appendToStore($vars, STORE_VAR);
            }

434
435
436
            // If given: fire a sqlBefore query
            $this->evaluate->parse($formElement[FE_SQL_BEFORE]);

437
            // Upload Type: Simple or Advanced
438
439
440
            // If (isset($primaryRecord[$column])) { - see #5048 - isset does not deal correctly with NULL!
            if (array_key_exists($column, $primaryRecord)) {
                // 'Simple Upload': no special action needed, just process the current (maybe modified) value.
441
442
                if ($pathFileName !== false) {
                    $newValues[$column] = $pathFileName;
443
444
445
446
447
448
449
450

                    if (isset($primaryRecord[COLUMN_FILE_SIZE])) {
                        $newValues[COLUMN_FILE_SIZE] = $vars[VAR_FILE_SIZE];
                    }

                    if (isset($primaryRecord[COLUMN_MIME_TYPE])) {
                        $newValues[COLUMN_MIME_TYPE] = $vars[VAR_FILE_MIME_TYPE];
                    }
451
                }
452
453
            } elseif (isset($formElement[FE_IMPORT_TO_TABLE]) && !isset($formElement[FE_SLAVE_ID])) {
                // Excel import on nonexisting column -> no upload
454
455
            } else {
                // 'Advanced Upload'
456
                $this->doUploadSlave($formElement, $modeUpload);
457
            }
458
459
460
461

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

462
        }
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
463

464
        // Only used in 'Simple Upload'
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
465
        if (count($newValues) > 0) {
466
            $this->updateRecord($this->formSpec[F_TABLE_NAME], $newValues, $recordId, $this->formSpec[F_PRIMARY_KEY]);
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
467
        }
468
469
    }

470
    /**
471
472
     * Process all Upload FormElements for the given $recordId.
     * After processing, &$formValues will be updated with the final filename.
473
     *
Marc Egger's avatar
Marc Egger committed
474
475
     * @throws \CodeException
     * @throws \UserFormException
476
477
478
479
480
481
482
483
484
485
486
487
488
     */
    public function processAllImageCutFE() {

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

            $this->extractImageDataReplaceFile($formElement);
        }
    }

489
490
491
492
493
494
495
496

    /**
     * Iterates over all FE and checks all 'required' (mode & modeSql) FE.
     * If a required FE is empty, throw an exception.
     * Take care to remove all FE with modeSql='hidden'.
     *
     * Typically, the browser does not allow a submit if a required field is empty.
     *
Marc Egger's avatar
Marc Egger committed
497
498
499
500
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
501
502
503
504
505
506
507
508
509
     */
    public function checkRequiredHidden() {

        $requiredOff = ($this->store->getVar(F_MODE_GLOBAL, STORE_SIP) == F_MODE_REQUIRED_OFF);

        $clientValues = $this->store::getStore(STORE_FORM);

        foreach ($this->feSpecNative AS $key => $formElement) {

510
511
512
513
514
            // Do not check retype slave FE.
            if (isset($formElement[FE_RETYPE_SOURCE_NAME])) {
                continue;
            }

515
516
            $this->store->setVar(SYSTEM_FORM_ELEMENT, "Column: " . $formElement[FE_NAME], STORE_SYSTEM);

517
518
519
520
521
522
523
            if (empty($formElement[FE_MODE_SQL])) {
                $mode = $formElement[FE_MODE];
            } else {
                $mode = $this->evaluate->parse($formElement[FE_MODE_SQL]);
                $this->feSpecNative[$key][FE_MODE_SQL] = $mode;
            }

524
525
526
            if (isset($formElement[FE_ACCEPT_ZERO_AS_REQUIRED]) && $formElement[FE_ACCEPT_ZERO_AS_REQUIRED] != '0' &&
                isset($clientValues[$formElement[FE_NAME]]) && $clientValues[$formElement[FE_NAME]] == '0') {
                $mode = 'fake'; // The next if() should never be true.
527
528
            }

529
            if (!$requiredOff && $mode == FE_MODE_REQUIRED && empty($clientValues[$formElement[FE_NAME]])) {
Marc Egger's avatar
Marc Egger committed
530
                throw new \UserFormException("Missing required value: " . $formElement[FE_LABEL], ERROR_REQUIRED_VALUE_EMPTY);
531
532
533
534
535
536
537
538
539
            }

            if ($mode == FE_MODE_HIDDEN) {
                // Removing the value from the store, forces that the value won't be stored.
                $this->store::unsetVar($formElement[FE_NAME], STORE_FORM);
            }
        }
    }

540
541
542
    /**
     *
     * @param array $formElement
Marc Egger's avatar
Marc Egger committed
543
544
     * @throws \CodeException
     * @throws \UserFormException
545
546
547
548
549
550
     */
    private function extractImageDataReplaceFile(array $formElement) {

        // Take care the necessary target directories exist.
        $cwd = getcwd();
        $sitePath = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
551
        if ($cwd === false || $sitePath === false || !HelperFile::chdir($sitePath)) {
Marc Egger's avatar
Marc Egger committed
552
            throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
553
                json_encode([ERROR_MESSAGE_TO_USER => 'getcwd() failed or SITE_PATH undefined or chdir() failed', ERROR_MESSAGE_TO_DEVELOPER => "getcwd() failed or SITE_PATH undefined or chdir('$sitePath') failed."]),
554
                ERROR_IO_CHDIR);
555
556
        }

557
        // Get original pathFileName
558
559
560
        $field = HelperFormElement::AppendFormElementNameImageCut($formElement);
        $pathFileName = $this->store->getVar($field, STORE_SIP);
        if ($pathFileName == '' || !file_exists($pathFileName)) {
Marc Egger's avatar
Marc Egger committed
561
            throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
562
                json_encode([ERROR_MESSAGE_TO_USER => 'Empty file or file not found', ERROR_MESSAGE_TO_DEVELOPER => 'Empty file or file not found: ' . $pathFileName]),
563
                ERROR_IO_FILE_NOT_FOUND);
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
        }

        // 'data:image/png;base64,AAAFBfj42Pj4...';
        $data = $this->store->getVar($formElement[FE_NAME], STORE_FORM, SANITIZE_ALLOW_ALLBUT);
        // Replace data by pathFileName (that is stored in DB).
        $this->store->setVar($formElement[FE_NAME], $pathFileName, STORE_FORM, true);

        if ($data == '') {
            return; // Nothing to do
        }

        // Split base64 encoded image: 'data:image/png;base64,AAAFBfj42Pj4...'
        list($type, $imageData) = explode(';', $data, 2); // $type= 'data:image/png;', $imageData='base64,AAAFBfj42Pj4...'
        list(, $extension) = explode('/', $type); // $type='png'
        list(, $imageData) = explode(',', $imageData); // $imageData='AAAFBfj42Pj4...'

        // If undefined: set default. BTW: Defined and empty means "no original".
        if (!isset($formElement[FE_IMAGE_CUT_KEEP_ORIGINAL])) {
            $formElement[FE_IMAGE_CUT_KEEP_ORIGINAL] = FE_IMAGE_CUT_ORIGINAL_EXTENSION;
        }
        $extSave = $formElement[FE_IMAGE_CUT_KEEP_ORIGINAL];

        $pathParts = pathinfo($pathFileName);
        // Keep the original file?
        if ($extSave != '') {

            // In case the leading '.' is missing.
            if ($extSave[0] != ".") {
                $extSave = '.' . $extSave;
            }

            // Check if there is already an original - don't create an additional one.
            if (!file_exists($pathFileName . $extSave) &&
                !file_exists($pathParts['dirname'] . $pathParts['filename'] . $extSave)
            ) {
599
                HelperFile::rename($pathFileName, $pathFileName . $extSave);
600
601
602
603
604
605
606
607
            }
        }

        if ($extension != $pathParts['extension']) {
            $pathFileName .= "." . $extension;
        }

        if (false === file_put_contents($pathFileName, base64_decode($imageData))) {
Marc Egger's avatar
Marc Egger committed
608
            throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
609
                json_encode([ERROR_MESSAGE_TO_USER => 'Write new image failed', ERROR_MESSAGE_TO_DEVELOPER => "Write new image failed: $pathFileName"]),
610
                ERROR_IO_WRITE);
611
612
613
614
615
        }

        $this->store->setVar($formElement[FE_NAME], $pathFileName, STORE_FORM, true);
    }

616
    /**
617
618
619
     * 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
620
621
     * Check also: doc/CODING.md
     *
Carsten  Rose's avatar
Carsten Rose committed
622
623
624
625
     * @param array $formElement FormElement 'upload'
     * @param string $sipUpload SIP
     * @param Sip $sip
     * @param string $modeUpload UPLOAD_MODE_UNCHANGED | UPLOAD_MODE_NEW | UPLOAD_MODE_DELETEOLD |
Carsten  Rose's avatar
Carsten Rose committed
626
627
     *                            UPLOAD_MODE_DELETEOLD_NEW
     *
628
     * @return false|string New pathFilename or false on error
Marc Egger's avatar
Marc Egger committed
629
630
631
632
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
633
634
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
635
636
     * @internal param $recordId
     */
637
638
639
    private function doUpload($formElement, $sipUpload, Sip $sip, &$modeUpload) {
        $flagDelete = false;
        $modeUpload = UPLOAD_MODE_UNCHANGED;
640

641
        // Status information about upload file
642
643
644
645
646
        $statusUpload = $this->store->getVar($sipUpload, STORE_EXTRA);
        if ($statusUpload === false) {
            return false;
        }

647
        if (isset($formElement[FE_IMPORT_TO_TABLE]) && isset($statusUpload[FILES_TMP_NAME])) {
648
            // Import
649
            $tmpFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);
650
651
            $this->doImport($formElement, $tmpFile);
        }
652

653
        // Upload - Take care the necessary target directories exist.
654
655
        $cwd = getcwd();
        $sitePath = $this->store->getVar(SYSTEM_SITE_PATH, STORE_SYSTEM);
656
        if ($cwd === false || $sitePath === false || !HelperFile::chdir($sitePath)) {
Marc Egger's avatar
Marc Egger committed
657
            throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
658
                json_encode([ERROR_MESSAGE_TO_USER => 'getcwd() failed or SITE_PATH undefined or chdir() failed', ERROR_MESSAGE_TO_DEVELOPER => "getcwd() failed or SITE_PATH undefined or chdir('$sitePath') failed."]),
659
                ERROR_IO_CHDIR);
660
        }
661

662
663
664
665
666
        // Delete existing old file.
        if (isset($statusUpload[FILES_FLAG_DELETE]) && $statusUpload[FILES_FLAG_DELETE] == '1') {
            $arr = $sip->getVarsFromSip($sipUpload);
            $oldFile = $arr[EXISTING_PATH_FILE_NAME];
            if (file_exists($oldFile)) {
667
                //TODO: it might be possible to delete a file, which is referenced by another record - a check would be nice.
668
                HelperFile::unlink($oldFile, $this->qfqLogFilename);
669
670
671
672
673
674
675
676
677
678
679
            }
            $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;
        }

680
681
        Logger::logMessageWithPrefix(UPLOAD_LOG_PREFIX . ': modeUpload= ' . $modeUpload, $this->qfqLogFilename);

682
        // skip uploading the file, if this is an import without a specified file destination
683
684
        if (!isset($formElement[FE_IMPORT_TO_TABLE]) || isset($formElement[FE_FILE_DESTINATION])) {
            $pathFileName = $this->copyUploadFile($formElement, $statusUpload);
685
686
687
688

            $msg = UPLOAD_LOG_PREFIX . ': ';
            $msg .= ($pathFileName == '') ? 'Remove old upload / no new upload' : 'File "' . $statusUpload[FILES_TMP_NAME] . '" >> "' . $pathFileName . '"';
            Logger::logMessageWithPrefix($msg, $this->qfqLogFilename);
689
690
        }

691
        HelperFile::chdir($cwd);
692
693
694
695
696
697
698
699
700

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

        return $pathFileName;
    }

    /**
     * @param $formElement
701
     * @param $fileName
Marc Egger's avatar
Marc Egger committed
702
703
704
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
705
706
707
708
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     */
    private function doImport($formElement, $fileName) {
709
        $importNamedSheetsOnly = array();
710

711
        Support::setIfNotSet($formElement, FE_IMPORT_TYPE, FE_IMPORT_TYPE_AUTO);
712

713
714
715
716
        if (!empty($formElement[FE_IMPORT_NAMED_SHEETS_ONLY])) {
            $importNamedSheetsOnly = explode(',', $formElement[FE_IMPORT_NAMED_SHEETS_ONLY]);
        }

Carsten  Rose's avatar
Carsten Rose committed
717
        // Check for keywords which needs an explicit given document type.
718
719
720
721
722
        if ($formElement[FE_IMPORT_TYPE] === FE_IMPORT_TYPE_AUTO) {

            $list = [FE_IMPORT_LIST_SHEET_NAMES, FE_IMPORT_READ_DATA_ONLY, FE_IMPORT_LIST_SHEET_NAMES];
            foreach ($list as $token) {
                if (isset($formElement[$token])) {
Marc Egger's avatar
Marc Egger committed
723
                    throw new \UserFormException('If ' . $token .
724
725
726
727
728
                        ' is given, an explicit document type (like ' . FE_IMPORT_TYPE . '=xlsx) should be set.', ERROR_IMPORT_MISSING_EXPLICIT_TYPE);
                }
            }
        }

729
730
731
732
733
734
735
736
737
738
739
        switch ($formElement[FE_IMPORT_TYPE]) {
            case FE_IMPORT_TYPE_AUTO:
                $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($fileName);
                break;

            case FE_IMPORT_TYPE_XLS:
            case FE_IMPORT_TYPE_XLSX:
            case FE_IMPORT_TYPE_CSV:
            case FE_IMPORT_TYPE_ODS:
                $inputFileType = ucfirst($formElement[FE_IMPORT_TYPE]);
                $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType);
740
741
742
743
744
745
746
747
748
749
750
751
752

                // setReadDataOnly
                if (($formElement[FE_IMPORT_READ_DATA_ONLY] ?? '0') != '0') {
                    $reader->setReadDataOnly(true);
                }

                // setLoadSheetsOnly
                if (!empty ($importNamedSheetsOnly)) {
                    $reader->setLoadSheetsOnly($importNamedSheetsOnly);
                }

                if (($formElement[FE_IMPORT_LIST_SHEET_NAMES] ?? '0') != '0') {
                    $sheetNames = $reader->listWorksheetNames($fileName);
Marc Egger's avatar
Marc Egger committed
753
                    throw new \UserFormException("Worksheets: " . implode(', ', $sheetNames),
754
755
756
                        ERROR_IMPORT_LIST_SHEET_NAMES);
                }

757
758
759
760
                $spreadsheet = $reader->load($fileName);
                break;

            default:
Marc Egger's avatar
Marc Egger committed
761
                throw new \UserFormException("Unknown Excel import type: '" . $formElement[FE_IMPORT_TYPE] . "'.",
762
763
764
765
                    ERROR_UNKNOWN_EXCEL_IMPORT_TYPE);
        }

        $tableName = $formElement[FE_IMPORT_TO_TABLE];
766
        $regions = OnArray::trimArray(explode('|', $formElement[FE_IMPORT_REGION] ?? ''));
767
        $columnNames = OnArray::trimArray(explode(',', $formElement[FE_IMPORT_TO_COLUMNS] ?? ''));
768
769
770
771
        $importMode = $formElement[FE_IMPORT_MODE] ?? FE_IMPORT_MODE_APPEND;

        foreach ($regions as $region) {
            // region: tab, startColumn, startRow, endColumn, endRow
772
            $region = OnArray::trimArray(explode(',', $region));
773
774
775
776
777
778
779
780
781
782
783
784
785
786
            $tab = 1;
            if (!empty($region[0])) {
                $tab = $region[0];
            }

            try {
                if (is_numeric($tab)) {
                    $worksheet = $spreadsheet->getSheet($tab - 1); // 0-based
                } else {
                    $worksheet = $spreadsheet->getSheetByName($tab);
                    if ($worksheet === null) {
                        throw new \PhpOffice\PhpSpreadsheet\Exception(
                            "No sheet with the name '$tab' could be found."
                        );
787
788
                    }
                }
789
            } catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
Marc Egger's avatar
Marc Egger committed
790
                throw new \UserFormException($e->getMessage());
791
            }
792

793
794
795
796
797
798
799
800
801
            // Set up requested region
            $columnStart = '1';
            $columnEnd = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($worksheet->getHighestColumn());
            $rowStart = 1;
            $rowEnd = $worksheet->getHighestRow();
            if (!empty($region[1])) { // startColumn
                if (!is_numeric($region[1])) $region[1] = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($region[1]);
                if ($region[1] >= $columnStart && $region[1] <= $columnEnd) {
                    $columnStart = $region[1];
802
803
                }
            }
804
805
806
807
808
            if (!empty($region[3])) { // endColumn
                if (!is_numeric($region[3])) $region[3] = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($region[3]);
                if ($region[3] >= $columnStart && $region[3] <= $columnEnd) {
                    $columnEnd = $region[3];
                }
809
            }
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
            if (!empty($region[2]) && $region[2] >= $rowStart && $region[2] <= $rowEnd) {
                $rowStart = $region[2];
            }
            if (!empty($region[4]) && $region[4] >= $rowStart && $region[4] <= $rowEnd) {
                $rowEnd = $region[4];
            }
            // Read the specified region
            $rangeStr = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($columnStart) . $rowStart . ':' .
                \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($columnEnd) . $rowEnd;
            $worksheetData = $worksheet->rangeToArray($rangeStr, '', true, false);

            $columnDefinitionArr = [];
            $columnListArr = [];
            for ($column = $columnStart; $column <= $columnEnd; ++$column) {
                if (!empty($columnNames[$column - $columnStart])) {
                    $columnName = $columnNames[$column - $columnStart];
                } else {
                    $columnName = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($column);
828
                }
829
830
                $columnDefinitionArr[] = "`$columnName`   TEXT       NOT NULL  DEFAULT ''";
                $columnListArr[] = "$columnName";
831
832
            }

833
834
835
836
837
838
839
840
841
842
843
844
845
            // SQL time!
            $createTableSql = "CREATE TABLE IF NOT EXISTS `$tableName` (" .
                "`id`        INT(11)    NOT NULL  AUTO_INCREMENT," .
                implode(', ', $columnDefinitionArr) . ',' .
                "`modified`  TIMESTAMP  NOT NULL  DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," .
                "`created`   DATETIME   NOT NULL  DEFAULT CURRENT_TIMESTAMP," .
                "PRIMARY KEY (`id`) )" .
                "ENGINE = InnoDB  DEFAULT CHARSET = utf8 AUTO_INCREMENT = 0;";
            $this->db->sql($createTableSql);

            if ($importMode === FE_IMPORT_MODE_REPLACE) {
                $this->db->sql("TRUNCATE $tableName");
                $importMode = FE_IMPORT_MODE_APPEND;
846
            }
847

848
849
850
851
852
853
854
            // Import the data
            foreach ($worksheetData AS $rowIndex => $row) {
                $columnList = implode(',', $columnListArr);
                $paramPlaceholders = str_repeat('?,', count($worksheetData[0]) - 1) . '?';
                $insertSql = "INSERT INTO `$tableName` ($columnList) VALUES ($paramPlaceholders)";
                $this->db->sql($insertSql, ROW_REGULAR, $row);
            }
855
        }
856
857
858
    }

    /**
Carsten  Rose's avatar
Upload:    
Carsten Rose committed
859
860
861
862
     * Copy uploaded file from temporary location to final location.
     *
     * Check also: doc/CODING.md
     *
863
864
865
     * @param array $formElement
     * @param array $statusUpload
     * @return array|mixed|null|string
Marc Egger's avatar
Marc Egger committed
866
867
868
869
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
870
     */
871
    private function copyUploadFile(array $formElement, array $statusUpload) {
872
873
        $pathFileName = '';

Carsten  Rose's avatar
Upload:    
Carsten Rose committed
874
        if (!isset($statusUpload[FILES_TMP_NAME]) || $statusUpload[FILES_TMP_NAME] === '') {
875
876
877
878
            // nothing to upload: e.g. user has deleted a previous uploaded file.
            return '';
        }

879
880
        $srcFile = Support::extendFilename($statusUpload[FILES_TMP_NAME], UPLOAD_CACHED);

881
        if (isset($formElement[FE_FILE_DESTINATION])) {
882

883
            // Provide variable 'filename'. Might be substituted in $formElement[FE_PATH_FILE_NAME].
884
            $origFilename = Sanitize::safeFilename($statusUpload[FILES_NAME]);
885
            $this->store->appendToStore(HelperFile::pathinfo($origFilename), STORE_VAR);
886

887
            $pathFileName = $this->evaluate->parse($formElement[FE_FILE_DESTINATION]);
888
            $pathFileName = Sanitize::safeFilename($pathFileName, false, true); // Dynamically calculated pathFileName might contain invalid characters.
889
890
891

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

894
        if ($pathFileName === '') {
Marc Egger's avatar
Marc Egger committed
895
            throw new \UserFormException("Upload failed, no target '" . FE_FILE_DESTINATION . "' specified.", ERROR_NO_TARGET_PATH_FILE_NAME);
896
897
        }

898
899
900
901
902
903
904
        // If given, get chmodDir. Needs to be prefixed with a 0 (=octal) - it should not be quoted! Symbolic mode is not allowed. E.g.: 0660, or 01777
        if (empty($formElement[FE_FILE_CHMOD_DIR])) {
            $chmodDir = false;
        } else {
            $chmodDir = octdec($formElement[FE_FILE_CHMOD_DIR]);
        }

905
        $overwrite = isset($formElement[FE_FILE_REPLACE_MODE]) && $formElement[FE_FILE_REPLACE_MODE] == FE_FILE_REPLACE_MODE_ALWAYS;
906
        Support::moveFile($srcFile, $pathFileName, $overwrite, $chmodDir);
907

908
        // get chmodFile
909
910
        if (empty($formElement[FE_FILE_CHMOD_FILE])) {
            $chmodFile = false;
911
        } else {
912
            $chmodFile = octdec($formElement[FE_FILE_CHMOD_FILE]);
913
914
        }

915
        $this->autoOrient($formElement, $pathFileName);
916
        HelperFile::chmod($pathFileName, $chmodFile);
917

918
        $this->splitUpload($formElement, $pathFileName, $chmodFile, $statusUpload);
919

920
921
        return $pathFileName;
    }
922

923
924
    /**
     * If fe['autoOrient'] is given and the MimeType corresponds to fe['autoOrientMimeType']: the given {{pathFileName:V}} will be converted.
925
     * ImageMagick 'convert' seems to do a better job than GraficsMagick (Orientation is stable even if multiple times applied).
926
927
928
     *
     * @param array $formElement
     * @param $pathFileName
Marc Egger's avatar
Marc Egger committed
929
930
931
932
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
933
     */
934
    private function autoOrient(array $formElement, $pathFileName) {
935
936

        // 'autoOrient' wished?
937
        if (!isset($formElement[FE_FILE_AUTO_ORIENT]) || $formElement[FE_FILE_AUTO_ORIENT] == '0') {
938
939
940
941
            return; // No
        }

        // Upload has matching MimeType?
942
        $mimeTypeList = empty($formElement[FE_FILE_AUTO_ORIENT_MIME_TYPE]) ? 'image/jpeg,image/png,image/tiff' : $formElement[FE_FILE_AUTO_ORIENT_MIME_TYPE];
943
        if (!HelperFile::checkFileType($pathFileName, $pathFileName, $mimeTypeList)) {
944
945
946
947
948
949
950
951
            return;
        }

        // Get 'autoOrient' command
        $cmd = empty($formElement[FE_FILE_AUTO_ORIENT_CMD]) ? FE_FILE_AUTO_ORIENT_CMD_DEFAULT : $formElement[FE_FILE_AUTO_ORIENT_CMD];
        $cmd = $this->evaluate->parse($cmd);

        // Do 'autoOrient' command
952
953
        $output = Support::qfqExec($cmd, $rc);
        if ($rc != 0) {
Marc Egger's avatar
Marc Egger committed
954
            throw new \UserFormException(json_encode([ERROR_MESSAGE_TO_USER => 'copy failed', ERROR_MESSAGE_TO_DEVELOPER => "[cmd=$cmd]$output"]), ERROR_IO_COPY);
955
        }
956
957
    }

958
    /**
Carsten  Rose's avatar
Carsten Rose committed
959
960
961
962
963
964
965
     * Check's if the file $pathFileName should be splitted in one file per page. If no: do nothing and return.
     * The only possible split target file format is 'svg': fileSplit=svg.
     * The splitted files will be saved under fileDestinationSplit=some/path/to/file.%02d.svg. A printf style token,
     * like '%02d', is needed to create distinguished filename's. See 'man pdf2svg' for further details.
     * For every created file, a record in table 'Split' is created (see splitSvg() ), storing the pathFileName of the
     * current page/file.
     *
966
     * @param array $formElement
967
968
     * @param string $pathFileName
     * @param int $chmod
969
     * @param array $statusUpload
Marc Egger's avatar
Marc Egger committed
970
971
972
973
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
974
     */
975
    private function splitUpload(array $formElement, $pathFileName, $chmod, array $statusUpload) {
976

977

978
979
980
981
982
983
984
985
986
        // Should the file be split?
        if (empty($formElement[FE_FILE_SPLIT])) {
            return;
        }

        // Is it technically possible to split the file?
        // BTW: the $statusUpload['type'] delivers 'octetstream' for PDF files who do not have the extension '.pdf' - therefore get mimetype again.
        $mime = HelperFile::getMimeType($pathFileName);
        if (false === strpos($mime, MIME_TYPE_SPLIT_CAPABLE) && $statusUpload[FILES_TYPE] != MIME_TYPE_SPLIT_CAPABLE) {
987
988
989
            return;
        }

990
991
992
993
        $fileDestinationSplit = $this->evaluate->parse($formElement[FE_FILE_DESTINATION_SPLIT] ?? '');
        $fileSplitType = $this->evaluate->parse($formElement[FE_FILE_SPLIT] ?? '');
        $fileSplitTypeOptions = $this->evaluate->parse($formElement[FE_FILE_SPLIT_OPTIONS] ?? '');
        $fileSplitTableName = $this->evaluate->parse($formElement[FE_FILE_SPLIT_TABLE_NAME] ?? '');
994
995
996
997
998

        if (empty($fileSplitTableName)) {
            $fileSplitTableName = $this->formSpec[F_TABLE_NAME];
        }

999
1000
1001
        if ($fileDestinationSplit == '') {
            $ext = ($fileSplitType == FE_FILE_SPLIT_SVG) ? '.%02d.svg' : '.jpg';
            $fileDestinationSplit = $pathFileName . '.split/split' . $ext;
1002
1003
        }

Carsten  Rose's avatar
Carsten Rose committed
1004
        HelperFile::mkDirParent($fileDestinationSplit);
1005
1006
1007
1008
1009

        // Save CWD
        $cwd = getcwd();

        // Create temporary directory
1010
        $tempDir = HelperFile::mktempdir();
1011
        $newSrc = $tempDir . DIRECTORY_SEPARATOR . QFQ_TEMP_SOURCE;
1012
        HelperFile::copy($pathFileName, $newSrc);
1013
1014
1015
1016

        // Split destination.
        $pathParts = pathinfo($fileDestinationSplit);
        if (empty($pathParts['filename']) || empty($pathParts['basename'])) {
Marc Egger's avatar
Marc Egger committed
1017
            throw new \UserFormException('Missing filename in ' . FE_FILE_DESTINATION_SPLIT, ERROR_MISSING_FILE_NAME);
1018
1019
1020
1021
1022
        }

        // Extract filename from destination directory.
        $fileNameDest = $pathParts['basename'];

1023
1024
1025
1026
1027
        switch ($fileSplitType) {
            case FE_FILE_SPLIT_SVG:
                $cmd = 'pdf2svg "' . $newSrc . '" "' . $fileNameDest . '" all';
                break;
            case FE_FILE_SPLIT_JPEG:
1028
1029
1030
1031
                if ($fileSplitTypeOptions == '') {
                    $fileSplitTypeOptions = FE_FILE_SPLIT_OPTIONS_JPEG;
                }
                $cmd = "convert $fileSplitTypeOptions '$newSrc' '$fileNameDest'";
1032
1033
                break;
            default:
Marc Egger's avatar
Marc Egger committed
1034
                throw new \UserFormException("Unknown 'fileSplit' type: " . $formElement[FE_FILE_SPLIT], ERROR_UNKNOWN_TOKEN);
1035
1036
        }

1037
        // Split PDF
1038
        HelperFile::chdir($tempDir);
1039
        $output = Support::qfqExec($cmd, $rc);
1040
1041
        HelperFile::chdir($cwd);

1042
        if ($rc != 0) {
Marc Egger's avatar
Marc Egger committed
1043
            throw new \UserFormException(
Marc Egger's avatar
Marc Egger committed
1044
                json_encode([ERROR_MESSAGE_TO_USER => 'pdf2svg failed', ERROR_MESSAGE_TO_DEVELOPER => "[$cwd][cmd=$cmd]$output"]),
1045
                ERROR_PDF2SVG);
1046
        }
1047

1048
        $files = Helperfile::getSplitFileNames($tempDir);
1049

1050
1051
1052
1053
1054
        $xId = $this->store->getVar(COLUMN_ID, STORE_RECORD);

        // Clean optional existing old split records and files from further uploads.
        $this->db->deleteSplitFileAndRecord($xId, $fileSplitTableName);

1055
1056
        // IM 'convert' will produce files <file>-1.jpg <file>-10.jpg ... - bring them in natural sort order
        natsort($files);
1057
1058
1059

        // Create DB records according to the extracted filenames.
        $tableName = TABLE_NAME_SPLIT;
1060
1061
1062
        $sql = "INSERT INTO $tableName (`tableName`, `xId`, `pathFilename`) VALUES (?,?,?)";

        // 1) Move split files to final location. 2) Created records to reference each split file.
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
        foreach ($files as $file) {

            if ($file == '.' || $file == '..' || $file == QFQ_TEMP_SOURCE) {
                continue;
            }

            if (!empty($pathParts['dirname'])) {
                $fileDestination = $pathParts['dirname'] . '/' . $file;
            } else {
                $fileDestination = $file;
            }

1075
            Support::moveFile($tempDir . DIRECTORY_SEPARATOR . $file, Support::joinPath($cwd, $fileDestination), true);
1076
            HelperFile::chmod($fileDestination, $chmod);
1077
1078

            // Insert records.
1079
            $this->db->sql($sql, ROW_REGULAR, [$fileSplitTableName, $xId, $fileDestination]);
1080
1081
1082
        }

        // Remove duplicated source
1083
1084
        HelperFile::unlink($newSrc);

1085
        // Remove empty directory
1086
        HelperFile::rmdir($tempDir);
1087
1088
    }

1089
    /**
1090
     * Create/update or delete the slave record.
1091
1092
     *
     * @param array $fe