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); |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
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(); |
|
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(); |
|
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 |