Issues (281)

Branch: master

src/Backend/Modules/Locale/Engine/Model.php (2 issues)

1
<?php
2
3
namespace Backend\Modules\Locale\Engine;
4
5
use Common\Uri as CommonUri;
6
use Backend\Core\Engine\Authentication as BackendAuthentication;
7
use Backend\Core\Language\Language as BL;
8
use Backend\Core\Engine\Model as BackendModel;
9
use SpoonFilter;
10
11
/**
12
 * In this file we store all generic functions that we will be using in the locale module
13
 */
14
class Model
15
{
16
    /**
17
     * @var array The possible locale types
18
     */
19
    public const TYPES = [
20
        'act',
21
        'err',
22
        'lbl',
23
        'msg',
24
    ];
25
26 3
    public static function buildCache(string $language, string $application): void
27
    {
28 3
        $cacheBuilder = new CacheBuilder(BackendModel::get('database'));
29 3
        $cacheBuilder->buildCache($language, $application);
30 3
    }
31
32
    public static function buildUrlQueryByFilter(array $filter): string
33
    {
34
        $query = http_build_query($filter, null, '&', PHP_QUERY_RFC3986);
35
        if ($query != '') {
36
            $query = '&' . $query;
37
        }
38
39
        return $query;
40
    }
41
42
    public static function createXMLForExport(array $items): string
43
    {
44
        $charset = BackendModel::getContainer()->getParameter('kernel.charset');
45
        $xml = new \DOMDocument('1.0', $charset);
46
47
        // set some properties
48
        $xml->preserveWhiteSpace = false;
49
        $xml->formatOutput = true;
50
51
        // locale root element
52
        $root = $xml->createElement('locale');
53
        $xml->appendChild($root);
54
55
        // loop applications
56
        foreach ($items as $application => $modules) {
57
            // create application element
58
            $applicationElement = $xml->createElement($application);
59
            $root->appendChild($applicationElement);
60
61
            // loop modules
62
            foreach ($modules as $module => $types) {
63
                // create application element
64
                $moduleElement = $xml->createElement($module);
65
                $applicationElement->appendChild($moduleElement);
66
67
                // loop types
68
                foreach ($types as $type => $items) {
69
                    // loop items
70
                    foreach ($items as $name => $translations) {
71
                        // create application element
72
                        $itemElement = $xml->createElement('item');
73
                        $moduleElement->appendChild($itemElement);
74
75
                        // attributes
76
                        $itemElement->setAttribute('type', self::getTypeName($type));
77
                        $itemElement->setAttribute('name', $name);
78
79
                        // loop translations
80
                        foreach ($translations as $translation) {
81
                            // create translation
82
                            $translationElement = $xml->createElement('translation');
83
                            $itemElement->appendChild($translationElement);
84
85
                            // attributes
86
                            $translationElement->setAttribute('language', $translation['language']);
87
88
                            // set content
89
                            $translationElement->appendChild(new \DOMCdataSection($translation['value']));
90
                        }
91
                    }
92
                }
93
            }
94
        }
95
96
        return $xml->saveXML();
97
    }
98
99
    /**
100
     * Delete (multiple) items from locale
101
     *
102
     * @param int[] $ids The id(s) to delete.
103
     */
104
    public static function delete(array $ids): void
105
    {
106
        // loop and cast to integers
107
        foreach ($ids as &$id) {
108
            $id = (int) $id;
109
        }
110
111
        // create an array with an equal amount of questionmarks as ids provided
112
        $idPlaceHolders = array_fill(0, count($ids), '?');
113
114
        // delete records
115
        BackendModel::getContainer()->get('database')->delete(
116
            'locale',
117
            'id IN (' . implode(', ', $idPlaceHolders) . ')',
118
            $ids
119
        );
120
121
        // rebuild cache
122
        self::buildCache(BL::getWorkingLanguage(), 'Backend');
123
        self::buildCache(BL::getWorkingLanguage(), 'Frontend');
124
    }
125
126
    public static function exists(int $id): bool
127
    {
128
        return (bool) BackendModel::getContainer()->get('database')->getVar(
129
            'SELECT 1
130
             FROM locale
131
             WHERE id = ?
132
             LIMIT 1',
133
            [$id]
134
        );
135
    }
136
137
    public static function existsByName(
138
        string $name,
139
        string $type,
140
        string $module,
141
        string $language,
142
        string $application,
143
        int $excludedId = null
144
    ): bool {
145
        // get database
146
        $database = BackendModel::getContainer()->get('database');
147
148
        // return
149
        if ($excludedId !== null) {
150
            return (bool) $database->getVar(
151
                'SELECT 1
152
                 FROM locale
153
                 WHERE name = ? AND type = ? AND module = ? AND language = ? AND application = ? AND id != ?
154
                 LIMIT 1',
155
                [$name, $type, $module, $language, $application, $excludedId]
156
            );
157
        }
158
159
        return (bool) BackendModel::getContainer()->get('database')->getVar(
160
            'SELECT 1
161
             FROM locale
162
             WHERE name = ? AND type = ? AND module = ? AND language = ? AND application = ?
163
             LIMIT 1',
164
            [$name, $type, $module, $language, $application]
165
        );
166
    }
167
168
    public static function get(int $id): array
169
    {
170
        // fetch record from database
171
        $record = (array) BackendModel::getContainer()->get('database')->getRecord(
172
            'SELECT * FROM locale WHERE id = ?',
173
            [$id]
174
        );
175
176
        // actions are urlencoded
177
        if ($record['type'] === 'act') {
178
            $record['value'] = urldecode($record['value']);
179
        }
180
181
        return $record;
182
    }
183
184
    public static function getByName(
185
        string $name,
186
        string $type,
187
        string $module,
188
        string $language,
189
        string $application
190
    ): int {
191
        return BackendModel::getContainer()->get('database')->getVar(
192
            'SELECT l.id
193
             FROM locale AS l
194
             WHERE name = ? AND type = ? AND module = ? AND language = ? AND application = ?',
195
            [$name, $type, $module, $language, $application]
196
        );
197
    }
198
199
    public static function getLanguagesForMultiCheckbox(bool $includeInterfaceLanguages = false): array
200
    {
201
        // get working languages
202
        $aLanguages = BL::getWorkingLanguages();
203
204
        // add the interface languages if needed
205
        if ($includeInterfaceLanguages) {
206
            $aLanguages = array_merge($aLanguages, BL::getInterfaceLanguages());
207
        }
208
209
        // create a new array to redefine the languages for the multicheckbox
210
        $languages = [];
211
212
        // loop the languages
213
        foreach ($aLanguages as $key => $lang) {
214
            // add to array
215
            $languages[$key]['value'] = $key;
216
            $languages[$key]['label'] = $lang;
217
        }
218
219
        return $languages;
220
    }
221
222
    public static function getTranslations(
223
        $application,
224
        string $module,
225
        array $types,
226
        array $languages,
227
        string $name,
228
        string $value
229
    ): array {
230
        // create an array for the languages, surrounded by quotes (example: 'en')
231
        $aLanguages = [];
232
        foreach ($languages as $key => $val) {
233
            $aLanguages[$key] = '\'' . $val . '\'';
234
        }
235
236
        // surround the types with quotes
237
        foreach ($types as $key => $val) {
238
            $types[$key] = '\'' . $val . '\'';
239
        }
240
241
        // get database
242
        $database = BackendModel::getContainer()->get('database');
243
244
        // build the query
245
        $query =
246
            'SELECT l.id, l.application, l.module, l.type, l.name, l.value, l.language, UNIX_TIMESTAMP(l.edited_on) as edited_on
247
             FROM locale AS l
248
             WHERE
249
                 l.language IN (' . implode(',', $aLanguages) . ') AND
250
                 l.name LIKE ? AND
251
                 l.value LIKE ? AND
252
                 l.type IN (' . implode(',', $types) . ')';
253
254
        // add the parameters
255
        $parameters = ['%' . $name . '%', '%' . $value . '%'];
256
257
        // add module to the query if needed
258
        if ($module) {
259
            $query .= ' AND l.module = ?';
260
            $parameters[] = $module;
261
        }
262
263
        // add module to the query if needed
264
        if ($application) {
265
            $query .= ' AND l.application = ?';
266
            $parameters[] = $application;
267
        }
268
269
        // get the translations
270
        $translations = (array) $database->getRecords($query, $parameters);
271
272
        // create an array for the sorted translations
273
        $sortedTranslations = [];
274
275
        // loop translations
276
        foreach ($translations as $translation) {
277
            // add to the sorted array
278
            $sortedTranslations[$translation['type']][$translation['name']][$translation['module']][$translation['language']] = [
279
                'id' => $translation['id'],
280
                'value' => $translation['value'],
281
                'edited_on' => $translation['edited_on'],
282
                'application' => $translation['application'],
283
            ];
284
        }
285
286
        // create an array to use in the datagrid
287
        $dataGridTranslations = [];
288
289
        // an id that is used for in the datagrid, this is not the id of the translation!
290
        $id = 0;
291
292
        // save the number of languages so this has not to be executed x number of times
293
        $numberOfLanguages = count($languages);
294
295
        // loop the sorted translations
296
        foreach ($sortedTranslations as $type => $references) {
297
            // create array for each type
298
            $dataGridTranslations[$type] = [];
299
300
            foreach ($references as $reference => $translation) {
301
                // loop modules
302
                foreach ($translation as $module => $t) {
303
                    // create translation (and increase id)
304
                    // we init the application here so it appears in front of the datagrid
305
                    $trans = [
306
                        'application' => '',
307
                        'module' => $module,
308
                        'name' => $reference,
309
                        'id' => $id++,
310
                    ];
311
312
                    // reset this var for every language
313
                    $edited_on = '';
314
315
                    foreach ($languages as $lang) {
316
                        // if the translation exists the for this language, fill it up
317
                        // else leave a space for the empty field
318
                        if (isset($t[$lang])) {
319
                            $trans[$lang] = $t[$lang]['value'];
320
                            $trans['application'] = $t[$lang]['application'];
321
322
                            // only alter edited_on if the date of a previously added date of another
323
                            // language is smaller
324
                            if ($edited_on < $t[$lang]['edited_on']) {
325
                                $edited_on = $t[$lang]['edited_on'];
326
                            }
327
328
                            if ($numberOfLanguages == 1) {
329
                                $trans['translation_id'] = $t[$lang]['id'];
330
                            } else {
331
                                $trans['translation_id_' . $lang] = $t[$lang]['id'];
332
                            }
333
                        } else {
334
                            $trans[$lang] = '';
335
336
                            if ($numberOfLanguages == 1) {
337
                                $trans['translation_id'] = '';
338
                            } else {
339
                                $trans['translation_id_' . $lang] = '';
340
                            }
341
                        }
342
                    }
343
                    // at the end of the array, add the generated edited_on date
344
                    $trans['edited_on'] = $edited_on;
345
346
                    // add the translation to the array
347
                    $dataGridTranslations[$type][] = $trans;
348
                }
349
            }
350
        }
351
352
        return $dataGridTranslations;
353
    }
354
355 1
    public static function getTypeName(string $type): string
356
    {
357
        // get full type name
358
        switch ($type) {
359 1
            case 'act':
360 1
                $type = 'action';
361 1
                break;
362 1
            case 'err':
363 1
                $type = 'error';
364 1
                break;
365 1
            case 'lbl':
366 1
                $type = 'label';
367 1
                break;
368 1
            case 'msg':
369 1
                $type = 'message';
370 1
                break;
371
        }
372
373 1
        return $type;
374
    }
375
376
    public static function getTypesForDropDown(): array
377
    {
378
        $labels = static::TYPES;
379
380
        // loop and build labels
381
        foreach ($labels as &$row) {
382
            $row = SpoonFilter::ucfirst(BL::msg(mb_strtoupper($row), 'Core'));
383
        }
384
385
        // build array
386
        return array_combine(static::TYPES, $labels);
387
    }
388
389
    public static function getTypesForMultiCheckbox(): array
390
    {
391
        $labels = static::TYPES;
392
393
        // loop and build labels
394
        foreach ($labels as &$row) {
395
            $row = SpoonFilter::ucfirst(BL::msg(mb_strtoupper($row), 'Core'));
396
        }
397
398
        // build array
399
        $aTypes = array_combine(static::TYPES, $labels);
400
401
        // create a new array to redefine the types for the multicheckbox
402
        $types = [];
403
404
        // loop the languages
405
        foreach ($aTypes as $key => $type) {
406
            // add to array
407
            $types[$key]['value'] = $key;
408
            $types[$key]['label'] = $type;
409
        }
410
411
        // return the redefined array
412
        return $types;
413
    }
414
415 1
    public static function importXML(
416
        \SimpleXMLElement $xml,
417
        bool $overwriteConflicts = false,
418
        array $frontendLanguages = null,
419
        array $backendLanguages = null,
420
        int $userId = null,
421
        string $date = null
422
    ): array {
423
        $statistics = [
424 1
            'total' => 0,
425
            'imported' => 0,
426
        ];
427
428
        // set defaults if necessary
429
        // we can't simply use these right away, because this function is also calls by the installer,
430
        // which does not have Backend-functions
431 1
        if ($frontendLanguages === null) {
432
            $frontendLanguages = array_keys(BL::getWorkingLanguages());
433
        }
434 1
        if ($backendLanguages === null) {
435
            $backendLanguages = array_keys(BL::getInterfaceLanguages());
436
        }
437 1
        if ($userId === null) {
438
            $userId = BackendAuthentication::getUser()->getUserId();
439
        }
440 1
        if ($date === null) {
441
            $date = BackendModel::getUTCDate();
442
        }
443
444
        // get database instance
445 1
        $database = BackendModel::getContainer()->get('database');
446
447
        // possible values
448 1
        $possibleApplications = ['Frontend', 'Backend'];
449 1
        $possibleModules = (array) $database->getColumn('SELECT m.name FROM modules AS m');
450
451
        // types
452 1
        $possibleTypes = [];
453 1
        foreach (static::TYPES as $type) {
454 1
            $possibleTypes[$type] = self::getTypeName($type);
455
        }
456
457
        // install English translations anyhow, they're fallback
458
        $possibleLanguages = [
459 1
            'Frontend' => array_unique(array_merge(['en'], $frontendLanguages)),
460 1
            'Backend' => array_unique(array_merge(['en'], $backendLanguages)),
461
        ];
462
463
        // current locale items (used to check for conflicts)
464 1
        $currentLocale = (array) $database->getColumn(
465 1
            'SELECT CONCAT(application, module, type, language, name)
466
             FROM locale'
467
        );
468
469
        // applications
470 1
        foreach ($xml as $application => $modules) {
471
            // application does not exist
472 1
            if (!in_array($application, $possibleApplications, true)) {
473
                continue;
474
            }
475
476
            // modules
477 1
            foreach ($modules as $module => $items) {
478
                // module does not exist
479 1
                if (!in_array($module, $possibleModules, true)) {
480
                    continue;
481
                }
482
483
                // items
484 1
                foreach ($items as $item) {
485
                    // attributes
486 1
                    $attributes = $item->attributes();
0 ignored issues
show
The method attributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

486
                    /** @scrutinizer ignore-call */ 
487
                    $attributes = $item->attributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
487 1
                    $type = SpoonFilter::getValue($attributes['type'], $possibleTypes, '');
488 1
                    $name = SpoonFilter::ucfirst(SpoonFilter::getValue($attributes['name'], null, ''));
489
490
                    // missing attributes
491 1
                    if ($type == '' || $name == '') {
492
                        continue;
493
                    }
494
495
                    // real type (shortened)
496 1
                    $type = array_search($type, $possibleTypes);
497
498
                    // translations
499 1
                    foreach ($item->translation as $translation) {
500
                        // statistics
501 1
                        ++$statistics['total'];
502
503
                        // attributes
504 1
                        $attributes = $translation->attributes();
0 ignored issues
show
The method attributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

504
                        /** @scrutinizer ignore-call */ 
505
                        $attributes = $translation->attributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
505 1
                        $language = SpoonFilter::getValue(
506 1
                            $attributes['language'],
507 1
                            $possibleLanguages[$application],
508 1
                            ''
509
                        );
510
511
                        // language does not exist
512 1
                        if ($language == '') {
513 1
                            continue;
514
                        }
515
516
                        // the actual translation
517 1
                        $translation = (string) $translation;
518
519
                        // locale item
520 1
                        $locale = [];
521 1
                        $locale['user_id'] = $userId;
522 1
                        $locale['language'] = $language;
523 1
                        $locale['application'] = $application;
524 1
                        $locale['module'] = $module;
525 1
                        $locale['type'] = $type;
526 1
                        $locale['name'] = $name;
527 1
                        $locale['value'] = $translation;
528 1
                        $locale['edited_on'] = $date;
529
530
                        // check if translation does not yet exist, or if the translation can be overridden
531 1
                        if (!in_array($application . $module . $type . $language . $name, $currentLocale)
532 1
                            || $overwriteConflicts
533
                        ) {
534 1
                            $database->execute(
535 1
                                'INSERT INTO locale (user_id, language, application, module, type, name, value, edited_on)
536
                                 VALUES (?, ?, ?, ?, ?, ?, ?, ?)
537
                                 ON DUPLICATE KEY UPDATE user_id = ?, value = ?, edited_on = ?',
538
                                [
539 1
                                    $locale['user_id'],
540 1
                                    $locale['language'],
541 1
                                    $locale['application'],
542 1
                                    $locale['module'],
543 1
                                    $locale['type'],
544 1
                                    $locale['name'],
545 1
                                    $locale['value'],
546 1
                                    $locale['edited_on'],
547 1
                                    $locale['user_id'],
548 1
                                    $locale['value'],
549 1
                                    $locale['edited_on'],
550
                                ]
551
                            );
552
553
                            // statistics
554 1
                            ++$statistics['imported'];
555
                        }
556
                    }
557
                }
558
            }
559
        }
560
561
        // rebuild cache
562 1
        foreach ($possibleApplications as $application) {
563 1
            foreach ($possibleLanguages[$application] as $language) {
564 1
                self::buildCache($language, $application);
565
            }
566
        }
567
568 1
        return $statistics;
569
    }
570
571
    public static function insert(array $item): int
572
    {
573
        // actions should be urlized
574
        if ($item['type'] == 'act' && urldecode($item['value']) != $item['value']) {
575
            $item['value'] = CommonUri::getUrl(
576
                $item['value']
577
            );
578
        }
579
580
        // insert item
581
        $item['id'] = (int) BackendModel::getContainer()->get('database')->insert('locale', $item);
582
583
        // rebuild the cache
584
        self::buildCache($item['language'], $item['application']);
585
586
        // return the new id
587
        return $item['id'];
588
    }
589
590
    public static function update(array $item): int
591
    {
592
        // actions should be urlized
593
        if ($item['type'] == 'act' && urldecode($item['value']) != $item['value']) {
594
            $item['value'] = CommonUri::getUrl(
595
                $item['value']
596
            );
597
        }
598
599
        // update category
600
        $updated = BackendModel::getContainer()->get('database')->update('locale', $item, 'id = ?', [$item['id']]);
601
602
        // rebuild the cache
603
        self::buildCache($item['language'], $item['application']);
604
605
        return $updated;
606
    }
607
}
608