dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
dbxContentLngSync.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxContent;
3
4require_once __DIR__ . '/dbxContent_bootstrap.php';
5require_once __DIR__ . '/dbxContentTranslate.class.php';
6require_once __DIR__ . '/dbxContent_permalink.class.php';
7
9
10 public static function masterLng(): string {
11 $lng = strtolower(trim((string) dbx()->get_config('dbx', 'default_lng', 'de')));
12 if ($lng === '' || $lng === 'undef') {
13 return 'de';
14 }
15 return $lng;
16 }
17
18 public static function accessibleLngs(): array {
19 if (function_exists('dbx_accessible_lngs')) {
20 return dbx_accessible_lngs();
21 }
22
23 return array(self::masterLng());
24 }
25
26 public static function isMasterLng(?string $lng = null): bool {
27 if ($lng === null || $lng === '') {
29 }
30
31 return strtolower(trim((string) $lng)) === self::masterLng();
32 }
33
34 public static function newUid(string $prefix = 'p'): string {
35 $prefix = preg_replace('/[^a-z]/', '', strtolower($prefix));
36 if ($prefix === '') {
37 $prefix = 'p';
38 }
39
40 try {
41 $rand = bin2hex(random_bytes(8));
42 } catch (\Throwable $e) {
43 $rand = substr(sha1(uniqid((string) mt_rand(), true)), 0, 16);
44 }
45
46 return $prefix . '_' . $rand;
47 }
48
49 public static function ensureSchema($db): void {
50 if (!is_object($db)) {
51 return;
52 }
53
54 $server = 'dbx|dbxContent.db3';
55 if (!$db->connect_db_server($server)) {
56 return;
57 }
58
59 foreach (self::accessibleLngs() as $lng) {
60 $contentTable = 'content_' . $lng;
61 $folderTable = 'content_folder_' . $lng;
62 if (!self::tableExists($db, $server, $contentTable) && !self::tableExists($db, $server, $folderTable)) {
63 continue;
64 }
65
66 self::ensureLngColumns($db, $server, $contentTable);
67 self::ensureLngColumns($db, $server, $folderTable);
68 self::ensureLngIndex($db, $server, $contentTable);
69 self::ensureLngIndex($db, $server, $folderTable);
70 }
71 }
72
73 public static function backfillMasterLng($db): void {
74 if (!is_object($db)) {
75 return;
76 }
77
78 self::ensureSchema($db);
79 $master = self::masterLng();
80 self::backfillTableUids($db, dbxContentLng::ddContent($master), 'p', true);
81 self::backfillTableUids($db, dbxContentLng::ddFolder($master), 'f', true);
82 }
83
84 public static function backfillUids($db): void {
85 if (!is_object($db)) {
86 return;
87 }
88
89 self::ensureSchema($db);
90
91 foreach (self::accessibleLngs() as $lng) {
92 $isMaster = $lng === self::masterLng();
93 self::backfillTableUids($db, dbxContentLng::ddContent($lng), 'p', $isMaster);
94 self::backfillTableUids($db, dbxContentLng::ddFolder($lng), 'f', $isMaster);
95 }
96 }
97
98 public static function ensureRecordUid($db, string $dd, int $id, string $prefix = 'p'): string {
99 $id = (int) $id;
100 if ($id <= 0 || !is_object($db)) {
101 return '';
102 }
103
104 $row = $db->select1($dd, $id, 'lng_uid,lng_sync,lng_rev,lng_synced_rev', 0);
105 if (!is_array($row)) {
106 return '';
107 }
108
109 $uid = trim((string) ($row['lng_uid'] ?? ''));
110 if ($uid !== '') {
111 return $uid;
112 }
113
114 $uid = self::newUid($prefix);
115 $sync = self::isMasterLng(self::lngFromDd($dd)) ? 'auto' : 'manual';
116 $db->update($dd, array(
117 'lng_uid' => $uid,
118 'lng_sync' => $sync,
119 'lng_rev' => 1,
120 'lng_synced_rev' => 0,
121 ), $id, 0, 1, 1, 0);
122
123 return $uid;
124 }
125
126 public static function afterPageSave($db, int $id, bool $isNew = false): void {
127 $id = (int) $id;
128 if ($id <= 0 || !is_object($db)) {
129 return;
130 }
131
133 self::ensureRecordUid($db, $dd, $id, 'p');
134
135 if (!self::isMasterLng()) {
136 self::markManual($db, $dd, $id);
137 return;
138 }
139
140 if ($isNew) {
141 $row = $db->select1($dd, $id, 'lng_rev,lng_sync', 0);
142 $rev = (int) ($row['lng_rev'] ?? 0);
143 if ($rev < 1) {
144 $db->update($dd, array('lng_rev' => 1, 'lng_sync' => 'auto'), $id, 0, 1, 1, 0);
145 }
146 } else {
147 $row = $db->select1($dd, $id, 'lng_rev', 0);
148 $rev = max(1, (int) ($row['lng_rev'] ?? 1)) + 1;
149 $db->update($dd, array('lng_rev' => $rev), $id, 0, 1, 1, 0);
150 }
151 }
152
153 public static function afterFolderSave($db, int $id, bool $isNew = false): void {
154 $id = (int) $id;
155 if ($id <= 0 || !is_object($db)) {
156 return;
157 }
158
160 self::ensureRecordUid($db, $dd, $id, 'f');
161
162 if (!self::isMasterLng()) {
163 self::markManual($db, $dd, $id);
164 return;
165 }
166
167 if ($isNew) {
168 $db->update($dd, array('lng_rev' => 1, 'lng_sync' => 'auto'), $id, 0, 1, 1, 0);
169 } else {
170 $row = $db->select1($dd, $id, 'lng_rev', 0);
171 $rev = max(1, (int) ($row['lng_rev'] ?? 1)) + 1;
172 $db->update($dd, array('lng_rev' => $rev), $id, 0, 1, 1, 0);
173 }
174 }
175
176 public static function markManual($db, string $dd, int $id): void {
177 $id = (int) $id;
178 if ($id <= 0 || !is_object($db)) {
179 return;
180 }
181
182 $db->update($dd, array('lng_sync' => 'manual'), $id, 0, 1, 1, 0);
183 }
184
185 public static function resolveIdByUid($db, string $dd, string $lngUid, string $lng = ''): int {
186 $lngUid = trim($lngUid);
187 if ($lngUid === '' || !is_object($db)) {
188 return 0;
189 }
190
191 if ($lng === '') {
192 $lng = dbxContentLng::current();
193 }
194
195 $dd = self::ddForLng($dd, $lng);
196 $rows = $db->select($dd, "lng_uid = '" . str_replace("'", "''", $lngUid) . "'", 'id', 'id', 'ASC', '', 1, 0, 0);
197 if (!is_array($rows) || !isset($rows[0]['id'])) {
198 return 0;
199 }
200
201 return (int) $rows[0]['id'];
202 }
203
204 public static function hasMissingSlaveLng($db, string $entity, int $masterId): bool {
205 $entity = $entity === 'folder' ? 'folder' : 'page';
206 $masterId = (int) $masterId;
207 if ($masterId <= 0 || !is_object($db) || !self::isMasterLng()) {
208 return false;
209 }
210
211 $slaveLngs = self::slaveLngs();
212 if (!count($slaveLngs)) {
213 return false;
214 }
215
216 $master = self::masterLng();
217 $masterDd = $entity === 'folder' ? dbxContentLng::ddFolder($master) : dbxContentLng::ddContent($master);
218 $masterRow = $db->select1($masterDd, $masterId, 'lng_uid', 0);
219 $lngUid = trim((string) ($masterRow['lng_uid'] ?? ''));
220 if ($lngUid === '') {
221 return true;
222 }
223
224 $dd = $entity === 'folder' ? dbxContentLng::ddFolder() : dbxContentLng::ddContent();
225 foreach ($slaveLngs as $lng) {
226 if (self::resolveIdByUid($db, $dd, $lngUid, $lng) <= 0) {
227 return true;
228 }
229 }
230
231 return false;
232 }
233
234 public static function coverageForUid($db, string $entity, string $lngUid): array {
235 $lngUid = trim($lngUid);
236 $entity = $entity === 'folder' ? 'folder' : 'page';
237 $out = array(
238 'lng_uid' => $lngUid,
239 'entity' => $entity,
240 'master_lng' => self::masterLng(),
241 'current_lng' => dbxContentLng::current(),
242 'languages' => array(),
243 );
244
245 if ($lngUid === '' || !is_object($db)) {
246 return $out;
247 }
248
249 $dd = $entity === 'folder' ? dbxContentLng::ddFolder() : dbxContentLng::ddContent();
250
251 foreach (self::accessibleLngs() as $lng) {
252 $lngDd = self::ddForLng($dd, $lng);
253 $row = self::selectByUid($db, $lngDd, $lngUid);
254 $status = 'missing';
255 $id = 0;
256 $title = '';
257 $sync = '';
258
259 if (is_array($row)) {
260 $id = (int) ($row['id'] ?? 0);
261 $sync = strtolower(trim((string) ($row['lng_sync'] ?? 'auto')));
262 if ($sync === '') {
263 $sync = 'auto';
264 }
265 $title = $entity === 'folder'
266 ? (string) ($row['name'] ?? '')
267 : (string) ($row['title'] ?? '');
268 $status = $lng === self::masterLng() ? 'master' : $sync;
269 }
270
271 $out['languages'][$lng] = array(
272 'lng' => $lng,
273 'status' => $status,
274 'id' => $id,
275 'title' => $title,
276 'lng_sync' => $sync,
277 'is_master' => $lng === self::masterLng() ? 1 : 0,
278 );
279 }
280
281 return $out;
282 }
283
284 public static function badgesHtml(array $coverage): string {
285 $languages = is_array($coverage['languages'] ?? null) ? $coverage['languages'] : array();
286 if (!count($languages)) {
287 return '';
288 }
289
290 $parts = array();
291 foreach ($languages as $lng => $item) {
292 if (!is_array($item)) {
293 continue;
294 }
295 $status = (string) ($item['status'] ?? 'missing');
296 $class = 'dbx-cms-lng-badge is-' . preg_replace('/[^a-z]/', '', $status);
297 $title = strtoupper($lng) . ': ' . $status;
298 if (!empty($item['title'])) {
299 $title .= ' — ' . $item['title'];
300 }
301 $parts[] = '<span class="' . $class . '" data-lng="' . htmlspecialchars($lng, ENT_QUOTES, 'UTF-8') . '" title="' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '">' . htmlspecialchars(strtoupper($lng), ENT_QUOTES, 'UTF-8') . '</span>';
302 }
303
304 if (!count($parts)) {
305 return '';
306 }
307
308 return '<span class="dbx-cms-lng-badges" aria-label="Sprachabdeckung">' . implode('', $parts) . '</span>';
309 }
310
311 public static function slaveLngs(): array {
312 $master = self::masterLng();
313 $out = array();
314 foreach (self::accessibleLngs() as $lng) {
315 if ($lng !== $master) {
316 $out[] = $lng;
317 }
318 }
319 return $out;
320 }
321
322 public static function resolveFolderIdInLng($db, int $masterFolderId, string $targetLng): int {
323 $masterFolderId = (int) $masterFolderId;
324 if ($masterFolderId <= 0 || !is_object($db)) {
325 return 0;
326 }
327
328 $row = $db->select1(dbxContentLng::ddFolder(self::masterLng()), $masterFolderId, 'lng_uid', 0);
329 if (!is_array($row)) {
330 return 0;
331 }
332
333 $uid = trim((string) ($row['lng_uid'] ?? ''));
334 if ($uid === '') {
335 return 0;
336 }
337
338 return self::resolveIdByUid($db, dbxContentLng::ddFolder(), $uid, $targetLng);
339 }
340
344 public static function ensureFolderIdInLng($db, int $masterFolderId, string $targetLng, int $depth = 0): int {
345 $masterFolderId = (int) $masterFolderId;
346 if ($masterFolderId <= 0 || !is_object($db) || $depth > 100) {
347 return 0;
348 }
349
350 $targetLng = strtolower(trim($targetLng));
351 $master = self::masterLng();
352 if ($targetLng === '' || $targetLng === $master) {
353 return $masterFolderId;
354 }
355
356 $existing = self::resolveFolderIdInLng($db, $masterFolderId, $targetLng);
357 if ($existing > 0) {
358 return $existing;
359 }
360
361 $masterDd = dbxContentLng::ddFolder($master);
362 $masterRow = $db->select1($masterDd, $masterFolderId);
363 if (!is_array($masterRow)) {
364 return 0;
365 }
366
367 $lngUid = trim((string) ($masterRow['lng_uid'] ?? ''));
368 if ($lngUid === '') {
369 $lngUid = self::ensureRecordUid($db, $masterDd, $masterFolderId, 'f');
370 }
371 if ($lngUid === '') {
372 return 0;
373 }
374
375 $existing = self::resolveIdByUid($db, dbxContentLng::ddFolder(), $lngUid, $targetLng);
376 if ($existing > 0) {
377 return $existing;
378 }
379
380 $parentTargetId = self::ensureFolderIdInLng($db, (int) ($masterRow['parent_id'] ?? 0), $targetLng, $depth + 1);
381
382 $name = dbxContentTranslate::translate((string) ($masterRow['name'] ?? ''), $master, $targetLng, 'folder_name');
383 if ($name === '' && trim((string) ($masterRow['name'] ?? '')) !== '') {
384 $name = (string) $masterRow['name'];
385 }
386 if ($name === '') {
387 $name = 'Ordner';
388 }
389
390 $masterRev = max(1, (int) ($masterRow['lng_rev'] ?? 1));
391 $data = self::copyFolderStructure($masterRow);
392 $data['name'] = $name;
393 $data['parent_id'] = $parentTargetId;
394 $data['lng_uid'] = $lngUid;
395 $data['lng_sync'] = 'auto';
396 $data['lng_rev'] = 0;
397 $data['lng_synced_rev'] = $masterRev;
398
399 $targetDd = dbxContentLng::ddFolder($targetLng);
400 if ($db->insert($targetDd, $data, 0, 1, 0, 1) !== 1) {
401 return 0;
402 }
403
404 return (int) $db->get_insert_id();
405 }
406
407 public static function previewProvision($db, string $entity, int $masterId, array $lngs = array()): array {
408 $entity = $entity === 'folder' ? 'folder' : 'page';
409 $masterId = (int) $masterId;
410 $master = self::masterLng();
411 $out = array(
412 'entity' => $entity,
413 'master_id' => $masterId,
414 'master_lng' => $master,
415 'lng_uid' => '',
416 'items' => array(),
417 );
418
419 if ($masterId <= 0 || !is_object($db) || !self::isMasterLng()) {
420 return $out;
421 }
422
423 $masterDd = $entity === 'folder' ? dbxContentLng::ddFolder($master) : dbxContentLng::ddContent($master);
424 $masterRow = $db->select1($masterDd, $masterId);
425 if (!is_array($masterRow)) {
426 return $out;
427 }
428
429 $lngUid = trim((string) ($masterRow['lng_uid'] ?? ''));
430 if ($lngUid === '') {
431 $lngUid = self::ensureRecordUid($db, $masterDd, $masterId, $entity === 'folder' ? 'f' : 'p');
432 }
433 $out['lng_uid'] = $lngUid;
434
435 if (!is_array($lngs) || !count($lngs)) {
436 $lngs = self::slaveLngs();
437 }
438
439 foreach ($lngs as $lng) {
440 $lng = strtolower(trim((string) $lng));
441 if ($lng === '' || $lng === $master) {
442 continue;
443 }
444
445 $targetDd = $entity === 'folder' ? dbxContentLng::ddFolder($lng) : dbxContentLng::ddContent($lng);
446 $existingId = self::resolveIdByUid($db, $targetDd, $lngUid, $lng);
447 $existing = $existingId > 0 ? $db->select1($targetDd, $existingId) : null;
448 $item = array(
449 'lng' => $lng,
450 'exists' => $existingId > 0 ? 1 : 0,
451 'id' => $existingId,
452 'lng_sync' => is_array($existing) ? (string) ($existing['lng_sync'] ?? 'auto') : '',
453 'enabled' => 1,
454 'warnings' => array(),
455 );
456
457 if ($entity === 'folder') {
458 $name = (string) ($masterRow['name'] ?? '');
459 $item['name'] = dbxContentTranslate::translate($name, $master, $lng, 'folder_name');
460 if ($item['name'] === '' && $name !== '') {
461 $item['name'] = $name;
462 }
463 } else {
464 $title = (string) ($masterRow['title'] ?? '');
465 $item['title'] = dbxContentTranslate::translate($title, $master, $lng, 'title');
466 if ($item['title'] === '' && $title !== '') {
467 $item['title'] = $title;
468 }
469 $item['description'] = dbxContentTranslate::translate((string) ($masterRow['description'] ?? ''), $master, $lng, 'description');
470 $item['keywords'] = dbxContentTranslate::translate((string) ($masterRow['keywords'] ?? ''), $master, $lng, 'keywords');
471 $item['content'] = dbxContentTranslate::translate((string) ($masterRow['content'] ?? ''), $master, $lng, 'content');
472
473 $folderId = self::resolveFolderIdInLng($db, (int) ($masterRow['folder'] ?? 0), $lng);
474 if ((int) ($masterRow['folder'] ?? 0) > 0 && $folderId <= 0) {
475 $item['warnings'][] = 'Ordnerstruktur in ' . strtoupper($lng) . ' wird bei Uebernahme automatisch angelegt.';
476 }
477 $item['folder'] = $folderId;
478 $item['permalink'] = dbxContent_permalink::build(
479 $db,
480 dbxContentLng::ddFolder($lng),
481 $folderId,
482 (string) $item['title']
483 );
484 }
485
486 $out['items'][] = $item;
487 }
488
489 return $out;
490 }
491
492 public static function provisionFromPreview($db, string $entity, int $masterId, array $items): array {
493 $entity = $entity === 'folder' ? 'folder' : 'page';
494 $masterId = (int) $masterId;
495 $result = array('ok' => 0, 'created' => array(), 'updated' => array(), 'errors' => array());
496
497 if ($masterId <= 0 || !is_object($db) || !self::isMasterLng()) {
498 $result['errors'][] = 'Nur in der Master-Sprache moeglich.';
499 return $result;
500 }
501
502 $master = self::masterLng();
503 $masterDd = $entity === 'folder' ? dbxContentLng::ddFolder($master) : dbxContentLng::ddContent($master);
504 $masterRow = $db->select1($masterDd, $masterId);
505 if (!is_array($masterRow)) {
506 $result['errors'][] = 'Master-Datensatz nicht gefunden.';
507 return $result;
508 }
509
510 $lngUid = trim((string) ($masterRow['lng_uid'] ?? ''));
511 if ($lngUid === '') {
512 $lngUid = self::ensureRecordUid($db, $masterDd, $masterId, $entity === 'folder' ? 'f' : 'p');
513 }
514
515 $masterRev = max(1, (int) ($masterRow['lng_rev'] ?? 1));
516
517 foreach ($items as $item) {
518 if (!is_array($item)) {
519 continue;
520 }
521 if ((int) ($item['enabled'] ?? 0) !== 1) {
522 continue;
523 }
524
525 $lng = strtolower(trim((string) ($item['lng'] ?? '')));
526 if ($lng === '' || $lng === $master) {
527 continue;
528 }
529
530 $targetDd = $entity === 'folder' ? dbxContentLng::ddFolder($lng) : dbxContentLng::ddContent($lng);
531 $existingId = self::resolveIdByUid($db, $targetDd, $lngUid, $lng);
532
533 if ($entity === 'folder') {
534 $name = trim((string) ($item['name'] ?? ''));
535 if ($name === '') {
536 $result['errors'][] = 'Name fuer ' . strtoupper($lng) . ' fehlt.';
537 continue;
538 }
539 $data = self::copyFolderStructure($masterRow);
540 $data['name'] = $name;
541 $data['lng_uid'] = $lngUid;
542 $data['lng_sync'] = 'auto';
543 $data['lng_rev'] = 0;
544 $data['lng_synced_rev'] = $masterRev;
545 $data['parent_id'] = self::ensureFolderIdInLng($db, (int) ($masterRow['parent_id'] ?? 0), $lng);
546 } else {
547 $title = trim((string) ($item['title'] ?? ''));
548 if ($title === '') {
549 $result['errors'][] = 'Titel fuer ' . strtoupper($lng) . ' fehlt.';
550 continue;
551 }
552 $folderId = (int) ($item['folder'] ?? 0);
553 if ($folderId <= 0) {
554 $folderId = self::ensureFolderIdInLng($db, (int) ($masterRow['folder'] ?? 0), $lng);
555 }
556 if ((int) ($masterRow['folder'] ?? 0) > 0 && $folderId <= 0) {
557 $result['errors'][] = 'Ordnerstruktur in ' . strtoupper($lng) . ' konnte nicht angelegt werden.';
558 continue;
559 }
560 $data = self::copyPageStructure($masterRow);
561 $data['activ'] = 0;
562 $data['folder'] = $folderId;
563 $data['title'] = $title;
564 $data['permalink'] = trim((string) ($item['permalink'] ?? ''));
565 if ($data['permalink'] === '') {
566 $data['permalink'] = dbxContent_permalink::build($db, dbxContentLng::ddFolder($lng), $folderId, $title);
567 }
568 $data['description'] = (string) ($item['description'] ?? '');
569 $data['keywords'] = (string) ($item['keywords'] ?? '');
570 $data['content'] = (string) ($item['content'] ?? '');
571 $data['lng_uid'] = $lngUid;
572 $data['lng_sync'] = 'auto';
573 $data['lng_rev'] = 0;
574 $data['lng_synced_rev'] = $masterRev;
575 }
576
577 if ($existingId > 0) {
578 $ok = $db->update($targetDd, $data, $existingId, 0, 1, 1, 0);
579 if ($ok === 1) {
580 $result['updated'][] = array('lng' => $lng, 'id' => $existingId);
581 } else {
582 $result['errors'][] = 'Update in ' . strtoupper($lng) . ' fehlgeschlagen.';
583 }
584 } else {
585 $ok = $db->insert($targetDd, $data, 0, 1, 0, 1);
586 if ($ok === 1) {
587 $newId = (int) $db->get_insert_id();
588 $result['created'][] = array('lng' => $lng, 'id' => $newId);
589 } else {
590 $result['errors'][] = 'Anlage in ' . strtoupper($lng) . ' fehlgeschlagen.';
591 }
592 }
593 }
594
595 $result['ok'] = (count($result['created']) || count($result['updated'])) ? 1 : 0;
596 return $result;
597 }
598
599 public static function syncSlavesFromMaster($db, string $entity, int $masterId): array {
600 $entity = $entity === 'folder' ? 'folder' : 'page';
601 $masterId = (int) $masterId;
602 $result = array('updated' => array(), 'skipped' => array(), 'errors' => array());
603
604 if ($masterId <= 0 || !is_object($db) || !self::isMasterLng()) {
605 return $result;
606 }
607
609
610 $master = self::masterLng();
611 $masterDd = $entity === 'folder' ? dbxContentLng::ddFolder($master) : dbxContentLng::ddContent($master);
612 $masterRow = $db->select1($masterDd, $masterId);
613 if (!is_array($masterRow)) {
614 $result['errors'][] = 'Master-Datensatz nicht gefunden.';
615 return $result;
616 }
617
618 $lngUid = trim((string) ($masterRow['lng_uid'] ?? ''));
619 if ($lngUid === '') {
620 return $result;
621 }
622
623 $masterRev = max(1, (int) ($masterRow['lng_rev'] ?? 1));
624
625 foreach (self::slaveLngs() as $lng) {
626 $targetDd = $entity === 'folder' ? dbxContentLng::ddFolder($lng) : dbxContentLng::ddContent($lng);
627 $slaveId = self::resolveIdByUid($db, $targetDd, $lngUid, $lng);
628
629 if ($slaveId <= 0) {
630 $result['skipped'][] = array('lng' => $lng, 'reason' => 'missing');
631 continue;
632 }
633
634 $slaveRow = $db->select1($targetDd, $slaveId);
635 if (!is_array($slaveRow)) {
636 $result['skipped'][] = array('lng' => $lng, 'reason' => 'not_found');
637 continue;
638 }
639
640 $sync = strtolower(trim((string) ($slaveRow['lng_sync'] ?? 'auto')));
641 if ($sync !== 'auto') {
642 $result['skipped'][] = array('lng' => $lng, 'reason' => 'manual');
643 continue;
644 }
645
646 $syncedRev = (int) ($slaveRow['lng_synced_rev'] ?? 0);
647 if ($syncedRev >= $masterRev) {
648 $result['skipped'][] = array('lng' => $lng, 'reason' => 'up_to_date');
649 continue;
650 }
651
652 if ($entity === 'folder') {
653 $name = dbxContentTranslate::translate((string) ($masterRow['name'] ?? ''), $master, $lng, 'folder_name');
654 if ($name === '' && trim((string) ($masterRow['name'] ?? '')) !== '') {
655 $name = (string) $masterRow['name'];
656 }
657 if ($name === '') {
658 $result['errors'][] = 'Name fuer ' . strtoupper($lng) . ' leer.';
659 continue;
660 }
661
662 $data = self::copyFolderStructure($masterRow);
663 $data['name'] = $name;
664 $data['parent_id'] = self::ensureFolderIdInLng($db, (int) ($masterRow['parent_id'] ?? 0), $lng);
665 $data['lng_synced_rev'] = $masterRev;
666 } else {
667 $title = dbxContentTranslate::translate((string) ($masterRow['title'] ?? ''), $master, $lng, 'title');
668 if ($title === '' && trim((string) ($masterRow['title'] ?? '')) !== '') {
669 $title = (string) $masterRow['title'];
670 }
671 if ($title === '') {
672 $result['errors'][] = 'Titel fuer ' . strtoupper($lng) . ' leer.';
673 continue;
674 }
675
676 $folderId = self::ensureFolderIdInLng($db, (int) ($masterRow['folder'] ?? 0), $lng);
677 if ((int) ($masterRow['folder'] ?? 0) > 0 && $folderId <= 0) {
678 $result['skipped'][] = array('lng' => $lng, 'reason' => 'folder_missing');
679 continue;
680 }
681
682 $data = self::copyPageStructure($masterRow);
683 $data['activ'] = (int) ($slaveRow['activ'] ?? 0);
684 $data['folder'] = $folderId;
685 $data['title'] = $title;
686 $data['description'] = dbxContentTranslate::translate((string) ($masterRow['description'] ?? ''), $master, $lng, 'description');
687 $data['keywords'] = dbxContentTranslate::translate((string) ($masterRow['keywords'] ?? ''), $master, $lng, 'keywords');
688 $data['content'] = dbxContentTranslate::translate((string) ($masterRow['content'] ?? ''), $master, $lng, 'content');
689 $data['permalink'] = dbxContent_permalink::build($db, dbxContentLng::ddFolder($lng), $folderId, $title);
690 $data['lng_synced_rev'] = $masterRev;
691 }
692
693 $ok = $db->update($targetDd, $data, $slaveId, 0, 1, 1, 0);
694 if ($ok === 1) {
695 $result['updated'][] = array('lng' => $lng, 'id' => $slaveId, 'entity' => $entity);
696 } else {
697 $result['errors'][] = 'Sync nach ' . strtoupper($lng) . ' fehlgeschlagen.';
698 }
699 }
700
701 return $result;
702 }
703
704 public static function folderDeletable($db, string $lng, int $folderId): array {
705 $folderId = (int) $folderId;
706 $out = array('deletable' => 0, 'reason' => '');
707
708 if ($folderId <= 0 || !is_object($db)) {
709 $out['reason'] = 'Ungueltiger Ordner.';
710 return $out;
711 }
712
713 $folderDd = dbxContentLng::ddFolder($lng);
714 $contentDd = dbxContentLng::ddContent($lng);
715 $childFolders = (int) $db->count($folderDd, 'parent_id = ' . $folderId);
716 $childPages = (int) $db->count($contentDd, 'folder = ' . $folderId);
717
718 if ($childFolders > 0 || $childPages > 0) {
719 $parts = array();
720 if ($childFolders > 0) {
721 $parts[] = $childFolders . ' Unterordner';
722 }
723 if ($childPages > 0) {
724 $parts[] = $childPages . ' Seite(n)';
725 }
726 $out['reason'] = implode(' und ', $parts) . ' vorhanden.';
727 return $out;
728 }
729
730 $out['deletable'] = 1;
731 return $out;
732 }
733
734 public static function previewDelete($db, string $entity, int $id): array {
735 $entity = $entity === 'folder' ? 'folder' : 'page';
736 $id = (int) $id;
737 $master = self::masterLng();
738 $current = dbxContentLng::current();
739 $out = array(
740 'entity' => $entity,
741 'id' => $id,
742 'lng_uid' => '',
743 'master_lng' => $master,
744 'current_lng' => $current,
745 'is_master' => self::isMasterLng() ? 1 : 0,
746 'items' => array(),
747 );
748
749 if ($id <= 0 || !is_object($db)) {
750 return $out;
751 }
752
753 $dd = $entity === 'folder' ? dbxContentLng::ddFolder() : dbxContentLng::ddContent();
754 $row = $db->select1($dd, $id);
755 if (!is_array($row)) {
756 return $out;
757 }
758
759 $lngUid = trim((string) ($row['lng_uid'] ?? ''));
760 if ($lngUid === '') {
761 $lngUid = self::ensureRecordUid($db, $dd, $id, $entity === 'folder' ? 'f' : 'p');
762 }
763 $out['lng_uid'] = $lngUid;
764
765 foreach (self::accessibleLngs() as $lng) {
766 $targetDd = $entity === 'folder' ? dbxContentLng::ddFolder($lng) : dbxContentLng::ddContent($lng);
767 $targetId = ($lng === $current) ? $id : self::resolveIdByUid($db, $targetDd, $lngUid, $lng);
768 if ($targetId <= 0) {
769 continue;
770 }
771
772 $targetRow = $db->select1($targetDd, $targetId);
773 if (!is_array($targetRow)) {
774 continue;
775 }
776
777 $sync = strtolower(trim((string) ($targetRow['lng_sync'] ?? 'auto')));
778 if ($sync === '') {
779 $sync = 'auto';
780 }
781
782 $label = $entity === 'folder'
783 ? (string) ($targetRow['name'] ?? '')
784 : (string) ($targetRow['title'] ?? '');
785 if ($label === '') {
786 $label = $entity === 'folder' ? 'Ordner #' . $targetId : 'Seite #' . $targetId;
787 }
788
789 $item = array(
790 'lng' => $lng,
791 'id' => $targetId,
792 'label' => $label,
793 'lng_sync' => $sync,
794 'is_master' => $lng === $master ? 1 : 0,
795 'checked' => ($lng === $master || $sync === 'auto') ? 1 : 0,
796 'deletable' => 1,
797 'block_reason' => '',
798 );
799
800 if ($entity === 'folder') {
801 $check = self::folderDeletable($db, $lng, $targetId);
802 $item['deletable'] = (int) ($check['deletable'] ?? 0);
803 $item['block_reason'] = (string) ($check['reason'] ?? '');
804 if ($item['deletable'] !== 1) {
805 $item['checked'] = 0;
806 }
807 }
808
809 $out['items'][] = $item;
810 }
811
812 return $out;
813 }
814
815 public static function resolveDeleteIds($db, string $entity, int $id, array $deleteLngs): array {
816 $entity = $entity === 'folder' ? 'folder' : 'page';
817 $id = (int) $id;
818 $out = array();
819
820 if ($id <= 0 || !is_object($db) || !count($deleteLngs)) {
821 return $out;
822 }
823
824 $current = dbxContentLng::current();
825 $dd = $entity === 'folder' ? dbxContentLng::ddFolder() : dbxContentLng::ddContent();
826 $row = $db->select1($dd, $id, 'lng_uid', 0);
827 if (!is_array($row)) {
828 return $out;
829 }
830
831 $lngUid = trim((string) ($row['lng_uid'] ?? ''));
832 if ($lngUid === '') {
833 $lngUid = self::ensureRecordUid($db, $dd, $id, $entity === 'folder' ? 'f' : 'p');
834 }
835
836 foreach ($deleteLngs as $lng) {
837 $lng = strtolower(trim((string) $lng));
838 if ($lng === '' || !in_array($lng, self::accessibleLngs(), true)) {
839 continue;
840 }
841
842 $targetDd = $entity === 'folder' ? dbxContentLng::ddFolder($lng) : dbxContentLng::ddContent($lng);
843 $targetId = ($lng === $current) ? $id : self::resolveIdByUid($db, $targetDd, $lngUid, $lng);
844 if ($targetId > 0) {
845 $out[] = array('lng' => $lng, 'id' => $targetId, 'entity' => $entity);
846 }
847 }
848
849 return $out;
850 }
851
852 public static function resetSyncToAuto($db, string $entity, int $masterId, array $lngs = array()): array {
853 $entity = $entity === 'folder' ? 'folder' : 'page';
854 $masterId = (int) $masterId;
855 $result = array('updated' => array(), 'skipped' => array(), 'errors' => array());
856
857 if ($masterId <= 0 || !is_object($db) || !self::isMasterLng()) {
858 $result['errors'][] = 'Nur in der Master-Sprache moeglich.';
859 return $result;
860 }
861
862 $master = self::masterLng();
863 $masterDd = $entity === 'folder' ? dbxContentLng::ddFolder($master) : dbxContentLng::ddContent($master);
864 $masterRow = $db->select1($masterDd, $masterId, 'lng_uid', 0);
865 if (!is_array($masterRow)) {
866 $result['errors'][] = 'Master-Datensatz nicht gefunden.';
867 return $result;
868 }
869
870 $lngUid = trim((string) ($masterRow['lng_uid'] ?? ''));
871 if ($lngUid === '') {
872 $lngUid = self::ensureRecordUid($db, $masterDd, $masterId, $entity === 'folder' ? 'f' : 'p');
873 }
874
875 if (!is_array($lngs) || !count($lngs)) {
876 $lngs = self::slaveLngs();
877 }
878
879 foreach ($lngs as $lng) {
880 $lng = strtolower(trim((string) $lng));
881 if ($lng === '' || $lng === $master) {
882 continue;
883 }
884
885 $targetDd = $entity === 'folder' ? dbxContentLng::ddFolder($lng) : dbxContentLng::ddContent($lng);
886 $slaveId = self::resolveIdByUid($db, $targetDd, $lngUid, $lng);
887 if ($slaveId <= 0) {
888 $result['skipped'][] = array('lng' => $lng, 'reason' => 'missing');
889 continue;
890 }
891
892 $ok = $db->update($targetDd, array('lng_sync' => 'auto'), $slaveId, 0, 1, 1, 0);
893 if ($ok === 1) {
894 $result['updated'][] = array('lng' => $lng, 'id' => $slaveId, 'entity' => $entity);
895 } else {
896 $result['errors'][] = 'Auto-Sync fuer ' . strtoupper($lng) . ' fehlgeschlagen.';
897 }
898 }
899
900 return $result;
901 }
902
903 public static function collectFolderSubtreeIds($db, int $rootFolderId): array {
904 $rootFolderId = (int) $rootFolderId;
905 if ($rootFolderId <= 0 || !is_object($db)) {
906 return array();
907 }
908
909 $master = self::masterLng();
910 $dd = dbxContentLng::ddFolder($master);
911 $ordered = array();
912 $seen = array();
913 $queue = array($rootFolderId);
914
915 while (count($queue)) {
916 $current = (int) array_shift($queue);
917 if ($current <= 0 || isset($seen[$current])) {
918 continue;
919 }
920 $seen[$current] = 1;
921 $ordered[] = $current;
922
923 $rows = $db->select($dd, 'parent_id = ' . $current, 'id', 'sorter,id', 'ASC', '', 0, 0, 0);
924 if (!is_array($rows)) {
925 continue;
926 }
927 foreach ($rows as $row) {
928 $id = (int) ($row['id'] ?? 0);
929 if ($id > 0 && !isset($seen[$id])) {
930 $queue[] = $id;
931 }
932 }
933 }
934
935 return $ordered;
936 }
937
938 public static function collectPageIdsInFolders($db, array $folderIds): array {
939 if (!is_object($db) || !count($folderIds)) {
940 return array();
941 }
942
943 $folderIds = array_values(array_filter(array_map('intval', $folderIds), function ($id) {
944 return $id > 0;
945 }));
946 if (!count($folderIds)) {
947 return array();
948 }
949
950 $master = self::masterLng();
951 $dd = dbxContentLng::ddContent($master);
952 $rows = $db->select($dd, 'folder IN (' . implode(',', $folderIds) . ')', 'id', 'sorter,id', 'ASC', '', 0, 0, 0);
953 if (!is_array($rows)) {
954 return array();
955 }
956
957 $out = array();
958 foreach ($rows as $row) {
959 $id = (int) ($row['id'] ?? 0);
960 if ($id > 0) {
961 $out[] = $id;
962 }
963 }
964 return $out;
965 }
966
967 public static function provisionFolderTree($db, int $masterFolderId, array $lngs = array()): array {
968 $masterFolderId = (int) $masterFolderId;
969 $result = array(
970 'ok' => 0,
971 'master_folder_id' => $masterFolderId,
972 'folders' => array(),
973 'pages' => array(),
974 'errors' => array(),
975 );
976
977 if ($masterFolderId <= 0 || !is_object($db) || !self::isMasterLng()) {
978 $result['errors'][] = 'Nur in der Master-Sprache moeglich.';
979 return $result;
980 }
981
982 $folderIds = self::collectFolderSubtreeIds($db, $masterFolderId);
983 if (!count($folderIds)) {
984 $result['errors'][] = 'Ordner nicht gefunden.';
985 return $result;
986 }
987
988 foreach ($folderIds as $folderId) {
989 $preview = self::previewProvision($db, 'folder', (int) $folderId, $lngs);
990 $items = array();
991 foreach (is_array($preview['items'] ?? null) ? $preview['items'] : array() as $item) {
992 if (!is_array($item)) {
993 continue;
994 }
995 $item['enabled'] = 1;
996 $items[] = $item;
997 }
998 $prov = self::provisionFromPreview($db, 'folder', (int) $folderId, $items);
999 $result['folders'][] = array(
1000 'master_id' => (int) $folderId,
1001 'result' => $prov,
1002 );
1003 if (is_array($prov['errors'] ?? null)) {
1004 $result['errors'] = array_merge($result['errors'], $prov['errors']);
1005 }
1006 }
1007
1008 $pageIds = self::collectPageIdsInFolders($db, $folderIds);
1009 foreach ($pageIds as $pageId) {
1010 $preview = self::previewProvision($db, 'page', (int) $pageId, $lngs);
1011 $items = array();
1012 foreach (is_array($preview['items'] ?? null) ? $preview['items'] : array() as $item) {
1013 if (!is_array($item)) {
1014 continue;
1015 }
1016 $item['enabled'] = 1;
1017 $items[] = $item;
1018 }
1019 $prov = self::provisionFromPreview($db, 'page', (int) $pageId, $items);
1020 $result['pages'][] = array(
1021 'master_id' => (int) $pageId,
1022 'result' => $prov,
1023 );
1024 if (is_array($prov['errors'] ?? null)) {
1025 $result['errors'] = array_merge($result['errors'], $prov['errors']);
1026 }
1027 }
1028
1029 $hasWork = count($result['folders']) || count($result['pages']);
1030 $result['ok'] = $hasWork ? 1 : 0;
1031 return $result;
1032 }
1033
1034 private static function copyPageStructure(array $masterRow): array {
1035 $skip = array('id', 'create_date', 'create_uid', 'update_date', 'update_uid', 'owner', 'title', 'permalink', 'description', 'keywords', 'content', 'lng_uid', 'lng_sync', 'lng_rev', 'lng_synced_rev');
1036 $data = array();
1037 foreach ($masterRow as $key => $value) {
1038 if (in_array($key, $skip, true)) {
1039 continue;
1040 }
1041 $data[$key] = $value;
1042 }
1043 return $data;
1044 }
1045
1046 private static function copyFolderStructure(array $masterRow): array {
1047 $skip = array('id', 'create_date', 'create_uid', 'update_date', 'update_uid', 'owner', 'name', 'parent_id', 'lng_uid', 'lng_sync', 'lng_rev', 'lng_synced_rev');
1048 $data = array();
1049 foreach ($masterRow as $key => $value) {
1050 if (in_array($key, $skip, true)) {
1051 continue;
1052 }
1053 $data[$key] = $value;
1054 }
1055 return $data;
1056 }
1057
1058 public static function renderLngBar(): string {
1059 $tpl = dbx()->get_system_obj('dbxTPL');
1060 $master = self::masterLng();
1061 $current = dbxContentLng::current();
1062 $items = array();
1063
1064 foreach (self::accessibleLngs() as $lng) {
1065 $active = $lng === $current ? ' is-active' : '';
1066 $isMaster = $lng === $master ? ' is-master' : '';
1067 $label = strtoupper($lng);
1068 if ($lng === $master) {
1069 $label .= ' *';
1070 }
1071 $url = '?dbx_modul=dbxContent_admin&dbx_run1=cms&dbx_lng=' . rawurlencode($lng);
1072 $items[] = '<a class="dbx-cms-lng-tab' . $active . $isMaster . '" href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" data-cms-lng="' . htmlspecialchars($lng, ENT_QUOTES, 'UTF-8') . '">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</a>';
1073 }
1074
1075 return $tpl->get_tpl('dbxContent_admin|cms-admin-lng-bar', array(
1076 'lng_bar_items' => implode('', $items),
1077 'master_lng' => $master,
1078 'current_lng' => $current,
1079 ));
1080 }
1081
1082 private static function backfillTableUids($db, string $dd, string $prefix, bool $isMaster = false): void {
1083 $rows = $db->select($dd, "(lng_uid IS NULL OR TRIM(lng_uid) = '')", 'id', 'id', 'ASC', '', 0, 0, 0);
1084 if (!is_array($rows)) {
1085 return;
1086 }
1087
1088 $sync = $isMaster ? 'auto' : 'manual';
1089
1090 foreach ($rows as $row) {
1091 if (!is_array($row)) {
1092 continue;
1093 }
1094 $id = (int) ($row['id'] ?? 0);
1095 if ($id <= 0) {
1096 continue;
1097 }
1098 $db->update($dd, array(
1099 'lng_uid' => self::newUid($prefix),
1100 'lng_sync' => $sync,
1101 'lng_rev' => 1,
1102 'lng_synced_rev' => 0,
1103 ), $id, 0, 1, 1, 0);
1104 }
1105 }
1106
1107 private static function selectByUid($db, string $dd, string $lngUid): ?array {
1108 $lngUid = str_replace("'", "''", trim($lngUid));
1109 if ($lngUid === '') {
1110 return null;
1111 }
1112
1113 $rows = $db->select($dd, "lng_uid = '" . $lngUid . "'", '*', 'id', 'ASC', '', 1, 0, 0);
1114 if (!is_array($rows) || !isset($rows[0]) || !is_array($rows[0])) {
1115 return null;
1116 }
1117
1118 return $rows[0];
1119 }
1120
1121 private static function ddForLng(string $dd, string $lng): string {
1122 if (strpos($dd, 'content_folder_') === 0 || $dd === dbxContentLng::ddFolder()) {
1123 return dbxContentLng::ddFolder($lng);
1124 }
1125
1126 return dbxContentLng::ddContent($lng);
1127 }
1128
1129 private static function lngFromDd(string $dd): string {
1130 if (preg_match('/_(de|en|es|[a-z]{2})$/', $dd, $m)) {
1131 return $m[1];
1132 }
1133
1134 return dbxContentLng::current();
1135 }
1136
1137 private static function ensureLngColumns($db, string $server, string $table): void {
1138 if (!self::tableExists($db, $server, $table)) {
1139 return;
1140 }
1141
1142 self::ensureColumn($db, $server, $table, 'lng_uid', "TEXT DEFAULT ''");
1143 self::ensureColumn($db, $server, $table, 'lng_sync', "TEXT DEFAULT 'auto'");
1144 self::ensureColumn($db, $server, $table, 'lng_rev', 'INTEGER DEFAULT 1');
1145 self::ensureColumn($db, $server, $table, 'lng_synced_rev', 'INTEGER DEFAULT 0');
1146 }
1147
1148 private static function ensureLngIndex($db, string $server, string $table): void {
1149 $table = preg_replace('/[^A-Za-z0-9_]+/', '', $table);
1150 if ($table === '') {
1151 return;
1152 }
1153
1154 $index = 'idx_' . $table . '_lng_uid';
1155 $exists = $db->select_query($server, "SELECT name FROM sqlite_master WHERE type='index' AND name='" . str_replace("'", "''", $index) . "'");
1156 if (is_array($exists) && !empty($exists)) {
1157 return;
1158 }
1159
1160 $db->exec($server, 'CREATE INDEX ' . $index . ' ON ' . $table . ' (lng_uid)');
1161 }
1162
1163 private static function ensureColumn($db, string $server, string $table, string $name, string $type): void {
1164 $table = preg_replace('/[^A-Za-z0-9_]+/', '', $table);
1165 $name = preg_replace('/[^A-Za-z0-9_]+/', '', $name);
1166 if ($table === '' || $name === '') {
1167 return;
1168 }
1169
1170 $cols = $db->select_query($server, 'PRAGMA table_info(' . $table . ')');
1171 if (is_array($cols)) {
1172 foreach ($cols as $col) {
1173 if (isset($col['name']) && strtolower((string) $col['name']) === strtolower($name)) {
1174 return;
1175 }
1176 }
1177 }
1178
1179 $db->exec($server, 'ALTER TABLE ' . $table . ' ADD COLUMN ' . $name . ' ' . $type);
1180 }
1181
1182 private static function tableExists($db, string $server, string $table): bool {
1183 $table = trim($table);
1184 if ($table === '') {
1185 return false;
1186 }
1187
1188 $rows = $db->select_query($server, "SELECT name FROM sqlite_master WHERE type='table' AND name='" . str_replace("'", "''", $table) . "' LIMIT 1");
1189 $row = is_array($rows) ? ($rows[0] ?? array()) : array();
1190 return is_array($row) && !empty($row['name']);
1191 }
1192}
$table['server']
Definition .dd.php:6
$index['name']
Definition .dd.php:162
static ddFolder(string $lng='')
static ddContent(string $lng='')
static resolveFolderIdInLng($db, int $masterFolderId, string $targetLng)
static coverageForUid($db, string $entity, string $lngUid)
static resetSyncToAuto($db, string $entity, int $masterId, array $lngs=array())
static resolveIdByUid($db, string $dd, string $lngUid, string $lng='')
static afterPageSave($db, int $id, bool $isNew=false)
static collectPageIdsInFolders($db, array $folderIds)
static provisionFromPreview($db, string $entity, int $masterId, array $items)
static folderDeletable($db, string $lng, int $folderId)
static ensureRecordUid($db, string $dd, int $id, string $prefix='p')
static previewDelete($db, string $entity, int $id)
static ensureFolderIdInLng($db, int $masterFolderId, string $targetLng, int $depth=0)
Ordner in Zielsprache aufloesen oder rekursiv aus Master-Struktur anlegen.
static collectFolderSubtreeIds($db, int $rootFolderId)
static markManual($db, string $dd, int $id)
static afterFolderSave($db, int $id, bool $isNew=false)
static syncSlavesFromMaster($db, string $entity, int $masterId)
static provisionFolderTree($db, int $masterFolderId, array $lngs=array())
static resolveDeleteIds($db, string $entity, int $id, array $deleteLngs)
static hasMissingSlaveLng($db, string $entity, int $masterId)
static previewProvision($db, string $entity, int $masterId, array $lngs=array())
if( $syncRequest)
Definition index.php:520
DBX schema administration.
if(! $db->connect_db_server($server)) $result
foreach( $topics as $file=> $html)