dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxDocsContentProvision.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxDocs;
3
4require_once dirname(__DIR__, 2) . '/dbxContent/include/dbxContent_bootstrap_sync.php';
5
6use DOMDocument;
7use DOMElement;
8use DOMXPath;
9use RuntimeException;
10use dbx\dbxContent\dbxContentLng;
11use dbx\dbxContent\dbxContentPageCache;
12use dbx\dbxContent\dbxContentPermalinkIndex;
13
23{
24 private object $db;
25 private string $baseDir;
26 private string $referenceDir;
27 private bool $force;
28 private array $folderIds = array();
29 private array $htmlToPermalink = array();
30 private array $result = array(
31 'folders_created' => 0,
32 'folders_updated' => 0,
33 'pages_created' => 0,
34 'pages_updated' => 0,
35 'pages_preserved' => 0,
36 'links_updated' => 0,
37 'errors' => array(),
38 );
39
40 public function __construct(object $db, string $baseDir, bool $force = false)
41 {
42 $this->db = $db;
43 $this->baseDir = rtrim(str_replace('\\', '/', $baseDir), '/');
44 $this->referenceDir = $this->baseDir . '/reference/current';
45 $this->force = $force;
46 }
47
48 public function run(): array
49 {
50 $this->prepareHtmlMap();
51 $this->prepareFolderTree();
52 $this->moveTutorialFolders();
53 $this->shortenTutorialTitles();
54 $this->provisionGermanDocumentation();
55 $this->provisionLegacyDocumentation();
56 $this->provisionServicePages();
57 $this->provisionCuratedDocumentationUpdates();
58 $this->repairDocumentationLinks();
59 dbxContentPageCache::invalidateAll();
60 return $this->result + array('ok' => $this->result['errors'] === array());
61 }
62
63 private function folderPlan(): array
64 {
65 return array(
66 'de' => array(
67 array('key' => 'root', 'name' => 'Dokumentation', 'parent' => '', 'sorter' => '0010'),
68 array('key' => 'start', 'name' => 'Einstieg', 'parent' => 'root', 'sorter' => '0010'),
69 array('key' => 'tutorials', 'name' => 'Tutorials', 'parent' => 'root', 'sorter' => '0020'),
70 array('key' => 'content', 'name' => 'Content & KI', 'parent' => 'root', 'sorter' => '0030'),
71 array('key' => 'design', 'name' => 'Design', 'parent' => 'root', 'sorter' => '0040'),
72 array('key' => 'shop', 'name' => 'Shop', 'parent' => 'root', 'sorter' => '0050'),
73 array('key' => 'workflow', 'name' => 'Workflows', 'parent' => 'root', 'sorter' => '0060'),
74 array('key' => 'development', 'name' => 'Entwicklung', 'parent' => 'root', 'sorter' => '0070'),
75 array('key' => 'architecture', 'name' => 'Architektur', 'parent' => 'development', 'sorter' => '0010'),
76 array('key' => 'libraries', 'name' => 'Kernbibliotheken', 'parent' => 'development', 'sorter' => '0020'),
77 array('key' => 'modules', 'name' => 'Module & JavaScript', 'parent' => 'development', 'sorter' => '0030'),
78 array('key' => 'deep', 'name' => 'Analysen & Konzepte', 'parent' => 'development', 'sorter' => '0040'),
79 array('key' => 'ai-instructions', 'name' => 'KI-Arbeitsanweisungen', 'parent' => 'development', 'sorter' => '0050'),
80 array('key' => 'operations', 'name' => 'Betrieb & Sicherheit', 'parent' => 'root', 'sorter' => '0080'),
81 array('key' => 'service', 'name' => 'Service', 'parent' => 'root', 'sorter' => '0090'),
82 ),
83 'en' => array(
84 array('key' => 'root', 'name' => 'Documentation', 'parent' => '', 'sorter' => '0010'),
85 array('key' => 'tutorials', 'name' => 'Tutorials', 'parent' => 'root', 'sorter' => '0020'),
86 array('key' => 'service', 'name' => 'Service', 'parent' => 'root', 'sorter' => '0090'),
87 ),
88 'es' => array(
89 array('key' => 'root', 'name' => 'Documentación', 'parent' => '', 'sorter' => '0010'),
90 array('key' => 'tutorials', 'name' => 'Tutoriales', 'parent' => 'root', 'sorter' => '0020'),
91 array('key' => 'service', 'name' => 'Servicio', 'parent' => 'root', 'sorter' => '0090'),
92 ),
93 );
94 }
95
96 private function pagePlan(): array
97 {
98 return array(
99 array('html' => 'dbxapp_user_docs.html', 'folder' => 'start', 'title' => 'Anwenderdokumentation', 'menu' => 'Anwenderhandbuch', 'permalink' => 'dokumentation-anwender'),
100 array('html' => 'dbxapp_user_start.html', 'folder' => 'start', 'title' => 'Einstieg und Bedienung', 'menu' => 'Erste Schritte', 'permalink' => 'dokumentation-erste-schritte'),
101 array('html' => 'dbxapp_user_admin.html', 'folder' => 'operations', 'title' => 'Administration und Systemstatus', 'menu' => 'Administration', 'permalink' => 'dokumentation-administration'),
102 array('html' => 'dbxapp_user_content.html', 'folder' => 'content', 'title' => 'Inhalte, CMS und Medien', 'menu' => 'CMS & Medien', 'permalink' => 'dokumentation-cms-medien'),
103 array('html' => 'dbxapp_user_shop.html', 'folder' => 'shop', 'title' => 'Shop bedienen und administrieren', 'menu' => 'Shop bedienen', 'permalink' => 'dokumentation-shop-bedienung'),
104 array('html' => 'dbxapp_user_workflows.html', 'folder' => 'workflow', 'title' => 'Workflows erstellen und benutzen', 'menu' => 'Workflows bedienen', 'permalink' => 'dokumentation-workflow-bedienung'),
105 array('html' => 'dbxapp_user_design.html', 'folder' => 'design', 'title' => 'Designs manuell und mit KI erstellen', 'menu' => 'Design-Tutorial', 'permalink' => 'dokumentation-design-tutorial'),
106 array('html' => 'dbxapp_user_ki_content.html', 'folder' => 'content', 'title' => 'KI-Bereich Content', 'menu' => 'KI: Content', 'permalink' => 'dokumentation-ki-content'),
107 array('html' => 'dbxapp_user_ki_design.html', 'folder' => 'design', 'title' => 'KI-Bereich Design', 'menu' => 'KI: Design', 'permalink' => 'dokumentation-ki-design'),
108 array('html' => 'dbxapp_user_ki_modules.html', 'folder' => 'modules', 'title' => 'KI-Bereich Module', 'menu' => 'KI: Module', 'permalink' => 'dokumentation-ki-module'),
109 array('html' => 'dbxapp_developer_docs.html', 'folder' => 'architecture', 'title' => 'Entwicklerdokumentation', 'menu' => 'Entwicklerhandbuch', 'permalink' => 'dokumentation-entwickler'),
110 array('html' => 'dbxapp_dev_orientation.html', 'folder' => 'architecture', 'title' => 'Architektur und Laufzeit', 'menu' => 'Architektur & Laufzeit', 'permalink' => 'dokumentation-architektur-laufzeit'),
111 array('html' => 'dbxapp_dev_modules.html', 'folder' => 'modules', 'title' => 'Verbindliche Modulentwicklung', 'menu' => 'Modulentwicklung', 'permalink' => 'dokumentation-modulentwicklung'),
112 array('html' => 'dbxapp_dev_pipelines.html', 'folder' => 'libraries', 'title' => 'Kernpipelines und Bibliotheken', 'menu' => 'Kernpipelines', 'permalink' => 'dokumentation-kernpipelines'),
113 array('html' => 'dbxapp_dev_content_design.html', 'folder' => 'architecture', 'title' => 'CMS, KI, Design und Fachmodule', 'menu' => 'Content & Fachmodule', 'permalink' => 'dokumentation-content-fachmodule'),
114 array('html' => 'dbxapp_dev_operations.html', 'folder' => 'operations', 'title' => 'Installation, Updates und Betrieb', 'menu' => 'Betrieb für Entwickler', 'permalink' => 'dokumentation-betrieb-entwickler'),
115 array('html' => 'dbxapp_system_overview.html', 'folder' => 'start', 'title' => 'Systemüberblick', 'menu' => 'Systemüberblick', 'permalink' => 'dokumentation-systemueberblick'),
116 array('html' => 'dbxapp_rad_runtime_ide.html', 'folder' => 'architecture', 'title' => 'RAD und Runtime-IDE', 'menu' => 'Runtime & IDE', 'permalink' => 'dokumentation-runtime-ide'),
117 array('html' => 'dbxapp_routing_templates.html', 'folder' => 'architecture', 'title' => 'Routing, Templates und Modul-Inclusion', 'menu' => 'Routing & Content', 'permalink' => 'dokumentation-routing-content'),
118 array('html' => 'dbxapp_dbxtpl.html', 'folder' => 'libraries', 'title' => 'dbxTPL-Leitfaden', 'menu' => 'dbxTPL', 'permalink' => 'dokumentation-dbxtpl'),
119 array('html' => 'dbxapp_dbxdb_dd_fd.html', 'folder' => 'libraries', 'title' => 'dbxDB, DD und FD', 'menu' => 'dbxDB, DD & FD', 'permalink' => 'dokumentation-dbxdb-dd-fd'),
120 array('html' => 'dbxapp_dbxform.html', 'folder' => 'libraries', 'title' => 'dbxForm-Leitfaden', 'menu' => 'dbxForm', 'permalink' => 'dokumentation-dbxform'),
121 array('html' => 'dbxapp_dbxreport.html', 'folder' => 'libraries', 'title' => 'dbxReport-Leitfaden', 'menu' => 'dbxReport', 'permalink' => 'dokumentation-dbxreport'),
122 array('html' => 'dbxapp_module_patterns.html', 'folder' => 'modules', 'title' => 'Module entwickeln', 'menu' => 'Modul-Patterns', 'permalink' => 'dokumentation-module-entwickeln'),
123 array('html' => 'dbxapp_javascript_libs.html', 'folder' => 'modules', 'title' => 'JavaScript-Systembibliotheken', 'menu' => 'JavaScript', 'permalink' => 'dokumentation-javascript'),
124 array('html' => 'dbxapp_cms_dbxki.html', 'folder' => 'content', 'title' => 'CMS und dbxKi', 'menu' => 'CMS & dbxKi', 'permalink' => 'dokumentation-cms-dbxki'),
125 array('html' => 'dbxapp_core_classes.html', 'folder' => 'libraries', 'title' => 'Core-Klassen', 'menu' => 'Core-Klassen', 'permalink' => 'dokumentation-core-klassen'),
126 array('html' => 'dbxapp_ai_rules.html', 'folder' => 'modules', 'title' => 'Verbindliche Regeln für KI-Agenten', 'menu' => 'KI-Regeln', 'permalink' => 'dokumentation-ki-regeln'),
127 array('html' => 'dbxapp_credits.html', 'folder' => 'service', 'title' => 'Bibliotheken, Credits und Lizenzen', 'menu' => 'Lizenzen', 'permalink' => 'dokumentation-lizenzen'),
128 array('html' => 'dbxapp_dbxinterpreter.html', 'folder' => 'libraries', 'title' => 'dbxInterpreter-Leitfaden', 'menu' => 'dbxInterpreter', 'permalink' => 'dokumentation-dbxinterpreter'),
129 array('html' => 'dbxapp_design_themes_skins.html', 'folder' => 'design', 'title' => 'Designs, Themes und Skins', 'menu' => 'Design-Grundlagen', 'permalink' => 'dokumentation-designs-skins'),
130 array('html' => 'dbxapp_design_ai_reference.html', 'folder' => 'design', 'title' => 'Design-KI-Referenz', 'menu' => 'Design per KI', 'permalink' => 'dokumentation-design-ki'),
131 array('html' => 'dbxapp_shop_guide.html', 'folder' => 'shop', 'title' => 'Shop-Leitfaden', 'menu' => 'Shop-Grundlagen', 'permalink' => 'dokumentation-shop'),
132 array('html' => 'dbxapp_shop_ai_reference.html', 'folder' => 'shop', 'title' => 'Shop-KI-Referenz', 'menu' => 'Shop per KI', 'permalink' => 'dokumentation-shop-ki'),
133 array('html' => 'dbxapp_workflow_guide.html', 'folder' => 'workflow', 'title' => 'dbxWorkflow-Leitfaden', 'menu' => 'Workflow-Grundlagen', 'permalink' => 'dokumentation-workflows'),
134 array('html' => 'dbxapp_workflow_create.html', 'folder' => 'workflow', 'title' => 'Workflow erstellen', 'menu' => 'Workflow erstellen', 'permalink' => 'dokumentation-workflow-erstellen'),
135 array('html' => 'dbxapp_workflow_use.html', 'folder' => 'workflow', 'title' => 'Workflow nutzen', 'menu' => 'Workflow nutzen', 'permalink' => 'dokumentation-workflow-nutzen'),
136 array('html' => 'dbxapp_current_operations.html', 'folder' => 'operations', 'title' => 'Aktueller Stand, Betrieb und Qualität', 'menu' => 'Betriebsstand', 'permalink' => 'dokumentation-betriebsstand'),
137 array('html' => 'dbxapp_security_integrity_performance.html', 'folder' => 'operations', 'title' => 'Sicherheit, Integrität und Performance', 'menu' => 'Sicherheit & Performance', 'permalink' => 'dokumentation-sicherheit-performance'),
138 array('html' => 'dbxapp_db_roundtrip.html', 'folder' => 'operations', 'title' => 'DB3–MySQL–DB3-Roundtrip', 'menu' => 'DB3 ↔ MySQL', 'permalink' => 'dokumentation-db3-mysql'),
139 array('html' => 'dbxapp_module_reference.html', 'folder' => 'modules', 'title' => 'Verbindliches Modulhandbuch', 'menu' => 'Modulhandbuch', 'permalink' => 'dokumentation-modulhandbuch'),
140 array('html' => 'dbxapp_design_studio_ki.html', 'folder' => 'design', 'title' => 'Design Studio und KI-Wizard', 'menu' => 'Design Studio', 'permalink' => 'dokumentation-design-studio'),
141 array('html' => 'dbxapp_install_update_dd_bindings.html', 'folder' => 'operations', 'title' => 'Installation, Updates und DD-Serverbindungen', 'menu' => 'Installation & Updates', 'permalink' => 'dokumentation-installation-updates'),
142 array('html' => 'dbxapp_ki_areas.html', 'folder' => 'content', 'title' => 'Die drei KI-Bereiche in dbxapp', 'menu' => 'KI-Bereiche', 'permalink' => 'dokumentation-ki-bereiche'),
143 array('html' => 'dbxapp_documentation_portal.html', 'folder' => 'operations', 'title' => 'Dokumentationsportal betreiben und veröffentlichen', 'menu' => 'Dokumentationsportal', 'permalink' => 'dokumentation-portal-betrieb'),
144 array('html' => 'dbxapp_user_system_update.html', 'folder' => 'operations', 'title' => 'System-Updates sicher durchführen', 'menu' => 'System-Update', 'permalink' => 'dokumentation-system-update'),
145 );
146 }
147
148 private function legacyPlan(): array
149 {
150 return array(
151 array('file' => 'files/doku/doku-anwendung.html', 'folder' => 'deep', 'title' => 'Anwendungsentwicklung mit dbxapp', 'menu' => 'Anwendungsentwicklung', 'permalink' => 'dokumentation-anwendungsentwicklung'),
152 array('file' => 'files/doku/doku-dbxapp-form-report-lifecycle-analyse.html', 'folder' => 'deep', 'title' => 'Form- und Report-Lifecycle', 'menu' => 'Form & Report Lifecycle', 'permalink' => 'dokumentation-form-report-lifecycle'),
153 array('file' => 'files/doku/doku-dbxapp-komponenten-stecknorm.html', 'folder' => 'deep', 'title' => 'Komponenten-Stecknorm', 'menu' => 'Komponenten-Stecknorm', 'permalink' => 'dokumentation-komponenten-stecknorm'),
154 array('file' => 'files/doku/doku-dbxapp-lego-rad-roadmap.html', 'folder' => 'deep', 'title' => 'LEGO-RAD-Roadmap', 'menu' => 'RAD-Roadmap', 'permalink' => 'dokumentation-rad-roadmap'),
155 array('file' => 'files/doku/doku-dbxapp-rad-konzept.html', 'folder' => 'deep', 'title' => 'RAD-Konzept', 'menu' => 'RAD-Konzept', 'permalink' => 'dokumentation-rad-konzept'),
156 array('file' => 'files/doku/doku-modulentwickler.html', 'folder' => 'deep', 'title' => 'Leitfaden für Modulentwickler', 'menu' => 'Modulentwickler', 'permalink' => 'dokumentation-modulentwickler'),
157 array('file' => 'files/doku/doku-system.html', 'folder' => 'deep', 'title' => 'Technische Systemdokumentation', 'menu' => 'Systemdokumentation', 'permalink' => 'dokumentation-system'),
158 array('file' => 'files/doku/md_files_2doku_2doku-dbxapp-libs-attribute.html', 'folder' => 'deep', 'title' => 'dbxapp-Library-Attribute', 'menu' => 'Library-Attribute', 'permalink' => 'dokumentation-library-attribute', 'reference' => 'md_files_2doku_2doku-dbxapp-libs-attribute.html'),
159 array('file' => 'files/doku/doku-codex-arbeitsanweisung-dbxapp.html', 'folder' => 'ai-instructions', 'title' => 'Codex-Arbeitsanweisung für dbxapp', 'menu' => 'Codex-Arbeitsanweisung', 'permalink' => 'dokumentation-codex-anweisung'),
160 array('file' => 'files/doku/doku-dbxapp-codex-arbeitsleitfaden.html', 'folder' => 'ai-instructions', 'title' => 'Codex-Arbeitsleitfaden', 'menu' => 'Codex-Leitfaden', 'permalink' => 'dokumentation-codex-leitfaden'),
161 array('file' => 'files/doku/chat-gpt-prompt-analyse.txt', 'folder' => 'ai-instructions', 'title' => 'KI-Prompt für Analysen', 'menu' => 'Prompt: Analyse', 'permalink' => 'dokumentation-prompt-analyse'),
162 array('file' => 'files/doku/chat-gpt-prompt-codex.txt', 'folder' => 'ai-instructions', 'title' => 'KI-Prompt für Codex', 'menu' => 'Prompt: Codex', 'permalink' => 'dokumentation-prompt-codex'),
163 array('file' => 'files/doku/chat-gpt-prompt-entwicklung.txt', 'folder' => 'ai-instructions', 'title' => 'KI-Prompt für Entwicklung', 'menu' => 'Prompt: Entwicklung', 'permalink' => 'dokumentation-prompt-entwicklung'),
164 );
165 }
166
167 private function prepareHtmlMap(): void
168 {
169 foreach ($this->pagePlan() as $page) {
170 $this->htmlToPermalink[strtolower((string)$page['html'])] = (string)$page['permalink'];
171 }
172 }
173
174 private function prepareFolderTree(): void
175 {
176 foreach ($this->folderPlan() as $language => $folders) {
177 foreach ($folders as $folder) {
178 $parentKey = (string)$folder['parent'];
179 $parentId = $parentKey === '' ? 0 : (int)($this->folderIds[$language][$parentKey] ?? 0);
180 $id = $this->ensureFolder(
181 $language,
182 (string)$folder['name'],
183 $parentId,
184 (string)$folder['sorter']
185 );
186 $this->folderIds[$language][(string)$folder['key']] = $id;
187 }
188 }
189 }
190
191 private function ensureFolder(string $language, string $name, int $parentId, string $sorter): int
192 {
193 $dd = dbxContentLng::ddFolder($language);
194 $row = $this->db->select1($dd, array('parent_id' => $parentId, 'name' => $name), 'id,name,parent_id,sorter', 0);
195 $id = (int)($row['id'] ?? 0);
196 if ($id > 0) {
197 return $id;
198 }
199
200 $created = $this->db->insert($dd, array(
201 'name' => $name,
202 'parent_id' => $parentId,
203 'group_read' => '*',
204 'sorter' => $sorter,
205 ), 0, 1, 0, 1);
206 if ($created !== 1) {
207 throw new RuntimeException('Ordner konnte nicht angelegt werden: ' . $language . '/' . $name);
208 }
209 $this->result['folders_created']++;
210 return (int)$this->db->get_insert_id();
211 }
212
213 private function moveTutorialFolders(): void
214 {
215 foreach (array('de', 'en', 'es') as $language) {
216 $dd = dbxContentLng::ddFolder($language);
217 $rows = $this->db->select($dd, '', 'id,name,parent_id,sorter', 'id', 'ASC', '', 0, 0, 0);
218 $targetId = (int)($this->folderIds[$language]['tutorials'] ?? 0);
219 $rootId = (int)($this->folderIds[$language]['root'] ?? 0);
220
221 foreach (is_array($rows) ? $rows : array() as $row) {
222 $id = (int)($row['id'] ?? 0);
223 $name = trim((string)($row['name'] ?? ''));
224 if ($id <= 0 || $id === $targetId || stripos($name, 'tutor') === false) {
225 continue;
226 }
227
228 $pages = $this->db->select(
229 dbxContentLng::ddContent($language),
230 array('folder' => $id),
231 'id',
232 'id',
233 'ASC',
234 '',
235 1,
236 0,
237 0
238 );
239 if (!is_array($pages) || $pages === array()) {
240 continue;
241 }
242
243 if ($this->folderIsEmpty($language, $targetId)) {
244 $this->db->delete($dd, $targetId, 0, 1);
245 $this->db->update($dd, array(
246 'name' => $language === 'es' ? 'Tutoriales' : 'Tutorials',
247 'parent_id' => $rootId,
248 'sorter' => '0020',
249 ), $id, 0, 1, 0, 1);
250 $this->folderIds[$language]['tutorials'] = $id;
251 $this->result['folders_updated']++;
252 }
253 break;
254 }
255 }
256 }
257
258 private function folderIsEmpty(string $language, int $folderId): bool
259 {
260 if ($folderId <= 0) {
261 return true;
262 }
263 $pages = $this->db->select(dbxContentLng::ddContent($language), array('folder' => $folderId), 'id', 'id', 'ASC', '', 1, 0, 0);
264 $folders = $this->db->select(dbxContentLng::ddFolder($language), array('parent_id' => $folderId), 'id', 'id', 'ASC', '', 1, 0, 0);
265 return (!is_array($pages) || $pages === array()) && (!is_array($folders) || $folders === array());
266 }
267
268 private function shortenTutorialTitles(): void
269 {
270 foreach (array('de', 'en', 'es') as $language) {
271 $folderId = (int)($this->folderIds[$language]['tutorials'] ?? 0);
272 if ($folderId <= 0) {
273 continue;
274 }
275 $dd = dbxContentLng::ddContent($language);
276 $rows = $this->db->select(
277 $dd,
278 array('folder' => $folderId),
279 'id,title,menu_title,seo_title',
280 'sorter,title,id',
281 'ASC',
282 '',
283 0,
284 0,
285 0
286 );
287 foreach (is_array($rows) ? $rows : array() as $row) {
288 $id = (int)($row['id'] ?? 0);
289 $fullTitle = trim((string)($row['seo_title'] ?? ''));
290 if ($fullTitle === '') {
291 $fullTitle = trim((string)($row['title'] ?? ''));
292 }
293 $menuTitle = $this->shortTutorialTitle((string)($row['title'] ?? ''));
294 if ($id <= 0 || $menuTitle === '') {
295 continue;
296 }
297 if (!$this->force && trim((string)($row['menu_title'] ?? '')) !== '') {
298 continue;
299 }
300 if ($this->db->update($dd, array(
301 'title' => $menuTitle,
302 'menu_title' => $menuTitle,
303 'seo_title' => $fullTitle,
304 ), $id, 0, 1, 0, 1) === 1) {
305 $this->result['pages_updated']++;
306 }
307 }
308 }
309 }
310
311 private function shortTutorialTitle(string $title): string
312 {
313 $title = trim((string)preg_replace('/^Tutorial\s*:\s*/iu', '', $title));
314 $replacements = array(
315 'Login, Profil und Passwort' => 'Login, Profil & Passwort',
316 'Designs manuell und mit dbxKi erstellen' => 'Design manuell & per KI',
317 'CMS Felder, Kopfleiste und Einstellungen' => 'CMS Felder & Einstellungen',
318 'CMS Medienverwendung' => 'CMS Medien',
319 'Shop im Frontend benutzen' => 'Shop-Frontend',
320 'Shop Dashboard' => 'Shop-Dashboard',
321 'Admin Dashboard' => 'Admin-Dashboard',
322 'Frontpage direkt bearbeiten' => 'Frontpage bearbeiten',
323 );
324 $title = $replacements[$title] ?? $title;
325 if (mb_strlen($title) <= 48) {
326 return $title;
327 }
328 $short = mb_substr($title, 0, 47);
329 $space = mb_strrpos($short, ' ');
330 if ($space !== false && $space > 28) {
331 $short = mb_substr($short, 0, $space);
332 }
333 return rtrim($short, " \t\n\r\0\x0B,.;:-") . '…';
334 }
335
336 private function provisionGermanDocumentation(): void
337 {
338 $sorterByFolder = array();
339 foreach ($this->pagePlan() as $page) {
340 $folderKey = (string)$page['folder'];
341 $sorterByFolder[$folderKey] = ($sorterByFolder[$folderKey] ?? 0) + 10;
342 try {
343 $content = $this->extractDoxygenContents((string)$page['html']);
344 $this->upsertPage('de', array(
345 'activ' => 1,
346 'folder' => (int)($this->folderIds['de'][$folderKey] ?? 0),
347 'title' => (string)$page['menu'],
348 'menu_title' => (string)$page['menu'],
349 'seo_title' => (string)$page['title'],
350 'permalink' => (string)$page['permalink'],
351 'description' => 'dbxapp-Dokumentation: ' . (string)$page['title'],
352 'keywords' => 'dbxapp, Dokumentation, dbxdocs-migration',
353 'group_read' => '*',
354 'sorter' => sprintf('%04d', $sorterByFolder[$folderKey]),
355 'template' => 'parent',
356 'content' => $content,
357 ));
358 } catch (\Throwable $exception) {
359 $this->result['errors'][] = (string)$page['html'] . ': ' . $exception->getMessage();
360 }
361 }
362 }
363
364 private function provisionLegacyDocumentation(): void
365 {
366 $sorterByFolder = array();
367 foreach ($this->legacyPlan() as $page) {
368 $folderKey = (string)$page['folder'];
369 $sorterByFolder[$folderKey] = ($sorterByFolder[$folderKey] ?? 0) + 10;
370 try {
371 $content = !empty($page['reference'])
372 ? $this->extractDoxygenContents((string)$page['reference'])
373 : $this->extractLegacyContents((string)$page['file']);
374 $this->upsertPage('de', array(
375 'activ' => 1,
376 'folder' => (int)($this->folderIds['de'][$folderKey] ?? 0),
377 'title' => (string)$page['menu'],
378 'menu_title' => (string)$page['menu'],
379 'seo_title' => (string)$page['title'],
380 'permalink' => (string)$page['permalink'],
381 'description' => 'Vertiefende dbxapp-Dokumentation: ' . (string)$page['title'],
382 'keywords' => 'dbxapp, Dokumentation, Vertiefung, dbxdocs-migration',
383 'group_read' => '*',
384 'sorter' => sprintf('%04d', $sorterByFolder[$folderKey]),
385 'template' => 'parent',
386 'content' => $content,
387 ));
388 } catch (\Throwable $exception) {
389 $this->result['errors'][] = (string)$page['file'] . ': ' . $exception->getMessage();
390 }
391 }
392 }
393
394 private function provisionServicePages(): void
395 {
396 $imprintDe = $this->db->select1(
397 dbxContentLng::ddContent('de'),
398 array('permalink' => 'impressum'),
399 'id,lng_uid',
400 0
401 );
402 $imprintUid = trim((string)($imprintDe['lng_uid'] ?? ''));
403 $this->movePageToService('de', (int)($imprintDe['id'] ?? 0), 'Impressum', '0080');
404
405 if ($imprintUid !== '') {
406 foreach (array('en' => 'Imprint', 'es' => 'Aviso legal') as $language => $menuTitle) {
407 $row = $this->db->select1(
408 dbxContentLng::ddContent($language),
409 array('lng_uid' => $imprintUid),
410 'id',
411 0
412 );
413 $this->movePageToService($language, (int)($row['id'] ?? 0), $menuTitle, '0080');
414 }
415 }
416
417 $contactPages = array(
418 'de' => array('title' => 'Kontakt', 'description' => 'Kontaktieren Sie das dbxapp-Team.', 'menu' => 'Kontakt'),
419 'en' => array('title' => 'Contact', 'description' => 'Contact the dbxapp team.', 'menu' => 'Contact'),
420 'es' => array('title' => 'Contacto', 'description' => 'Póngase en contacto con el equipo de dbxapp.', 'menu' => 'Contacto'),
421 );
422 foreach ($contactPages as $language => $labels) {
423 $this->upsertPage($language, array(
424 'activ' => 1,
425 'folder' => (int)($this->folderIds[$language]['service'] ?? 0),
426 'title' => $labels['title'],
427 'menu_title' => $labels['menu'],
428 'permalink' => 'dokumentation-kontakt-' . $language,
429 'description' => $labels['description'],
430 'keywords' => 'dbxapp, contact, dbxdocs-migration',
431 'group_read' => '*',
432 'sorter' => '0090',
433 'template' => 'parent',
434 'content' => '<p class="lead">' . dbx()->esc($labels['description']) . '</p>'
435 . '[modul=dbxContact]dbx_run1=form[/modul]',
436 ));
437 }
438 }
439
440 private function movePageToService(string $language, int $id, string $menuTitle, string $sorter): void
441 {
442 if ($id <= 0) {
443 return;
444 }
445 $updated = $this->db->update(dbxContentLng::ddContent($language), array(
446 'folder' => (int)($this->folderIds[$language]['service'] ?? 0),
447 'menu_title' => $menuTitle,
448 'sorter' => $sorter,
449 ), $id, 0, 1, 0, 1);
450 if ($updated === 1) {
451 $this->result['pages_updated']++;
452 }
453 }
454
461 private function provisionCuratedDocumentationUpdates(): void
462 {
463 $updates = array(
464 'dokumentation-installation' => array(
465 'revision' => '2026-08-01-installation-2',
466 'file' => dirname(__DIR__) . '/content/dbxapp_user_installation.html',
467 'page' => array(
468 'activ' => 1,
469 'folder_key' => 'operations',
470 'title' => 'dbxapp installieren',
471 'menu_title' => 'Installation',
472 'seo_title' => 'dbxapp installieren – Voraussetzungen und sieben Schritte',
473 'permalink' => 'dokumentation-installation',
474 'description' => 'dbxapp sicher installieren: Voraussetzungen, DB3 oder PDO, persönlicher Administrator und Abnahme.',
475 'keywords' => 'dbxapp, Installation, Setup, DB3, PDO, Administrator, dbxdocs-curated',
476 'group_read' => '*',
477 'sorter' => '0005',
478 'template' => 'parent',
479 ),
480 ),
481 'tutorial-installation' => array(
482 'revision' => '2026-08-01-tutorial-installation-2',
483 'file' => dirname(__DIR__) . '/content/tutorial_installation.html',
484 'page' => array(
485 'activ' => 1,
486 'folder_key' => 'tutorials',
487 'title' => 'Tutorial: dbxapp installieren',
488 'menu_title' => 'Installation',
489 'seo_title' => 'Tutorial: dbxapp Schritt für Schritt installieren',
490 'permalink' => 'tutorial-installation',
491 'description' => 'Geführte dbxapp-Installation mit Voraussetzungen, sieben Assistentenschritten, Kontrollpunkten, SelfTest und Fehlersuche.',
492 'keywords' => 'dbxapp, Tutorial, Installation, Setup, Administrator, SelfTest, dbxdocs-curated',
493 'group_read' => '*',
494 'sorter' => '0005',
495 'template' => 'parent',
496 ),
497 ),
498 'dokumentation-selbsttest' => array(
499 'revision' => '2026-08-01-selftest-2',
500 'file' => dirname(__DIR__) . '/content/dbxapp_user_selftest.html',
501 'page' => array(
502 'activ' => 1,
503 'folder_key' => 'operations',
504 'title' => 'System-Selbsttest',
505 'menu_title' => 'System-Selbsttest',
506 'seo_title' => 'dbxSelfTest – vollständige Systemtests und JavaScript-Prüfungen',
507 'permalink' => 'dokumentation-selbsttest',
508 'description' => 'dbxSelfTest vollständig erklärt: Schnell-, Komplett-, Auswahl- und Einzeltests, Browser-JavaScript, Protokolle und CLI.',
509 'keywords' => 'dbxapp, dbxSelfTest, Selbsttest, JavaScript, PHP, Qualitätssicherung, dbxdocs-curated',
510 'group_read' => '*',
511 'sorter' => '0015',
512 'template' => 'parent',
513 ),
514 ),
515 'dokumentation-ki-design' => array(
516 'revision' => '2026-07-28-ki-design-2',
517 'file' => dirname(__DIR__) . '/content/dbxapp_user_ki_design.html',
518 ),
519 );
520
521 $dd = dbxContentLng::ddContent('de');
522 foreach ($updates as $permalink => $update) {
523 $file = (string)$update['file'];
524 $content = is_file($file) ? file_get_contents($file) : false;
525 if (!is_string($content) || trim($content) === '') {
526 $this->result['errors'][] = basename($file) . ': Kuratierter CMS-Inhalt fehlt.';
527 continue;
528 }
529 $content = trim($content);
530 $row = $this->db->select1(
531 $dd,
532 array('permalink' => $permalink),
533 'id,content',
534 0
535 );
536 $id = (int)($row['id'] ?? 0);
537 $revision = (string)$update['revision'];
538 $current = (string)($row['content'] ?? '');
539 if ($id > 0 && str_contains($current, 'data-dbx-doc-revision="' . $revision . '"')) {
540 continue;
541 }
542
543 $page = is_array($update['page'] ?? null) ? $update['page'] : array();
544 if ($id <= 0) {
545 if ($page === array()) {
546 $this->result['errors'][] = $permalink . ': Metadaten für die neue CMS-Seite fehlen.';
547 continue;
548 }
549 $folderKey = (string)($page['folder_key'] ?? '');
550 unset($page['folder_key']);
551 $page['folder'] = (int)($this->folderIds['de'][$folderKey] ?? 0);
552 $page['content'] = $content;
553 try {
554 $this->upsertPage('de', $page);
555 } catch (\Throwable $exception) {
556 $this->result['errors'][] = $permalink . ': ' . $exception->getMessage();
557 }
558 continue;
559 }
560
561 $changes = array('content' => $content);
562 if ($page !== array()) {
563 $folderKey = (string)($page['folder_key'] ?? '');
564 unset($page['folder_key']);
565 $page['folder'] = (int)($this->folderIds['de'][$folderKey] ?? 0);
566 $changes = array_merge($page, $changes);
567 }
568 if ($this->db->update($dd, $changes, $id, 0, 1, 0, 1) === 1) {
569 $this->result['pages_updated']++;
570 dbxContentPermalinkIndex::upsertPage(
571 $id,
572 $permalink,
573 (string)($changes['group_read'] ?? '*'),
574 (int)($changes['activ'] ?? 1),
575 'de'
576 );
577 }
578 }
579 }
580
587 private function repairDocumentationLinks(): void
588 {
589 $dd = dbxContentLng::ddContent('de');
590 $rows = $this->db->select(
591 $dd,
592 '',
593 'id,permalink,content',
594 'id',
595 'ASC',
596 '',
597 0,
598 0,
599 0
600 );
601
602 foreach (is_array($rows) ? $rows : array() as $row) {
603 $id = (int)($row['id'] ?? 0);
604 $permalink = trim((string)($row['permalink'] ?? ''));
605 $content = (string)($row['content'] ?? '');
606 if (
607 $id <= 0
608 || $content === ''
609 || (!str_starts_with($permalink, 'dokumentation-') && !str_starts_with($permalink, 'tutorial-'))
610 ) {
611 continue;
612 }
613
614 $updated = preg_replace_callback(
615 '/\bhref=(["\'])(.*?)\1/isu',
616 function (array $match): string {
617 $href = html_entity_decode((string)$match[2], ENT_QUOTES | ENT_HTML5, 'UTF-8');
618 $mapped = $this->knownCmsHref($href);
619 if ($mapped === '') {
620 return (string)$match[0];
621 }
622 return 'href=' . (string)$match[1]
623 . htmlspecialchars($mapped, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
624 . (string)$match[1];
625 },
627 );
628
629 if (!is_string($updated) || $updated === $content) {
630 continue;
631 }
632 if ($this->db->update($dd, array('content' => $updated), $id, 0, 1, 0, 1) === 1) {
633 $this->result['links_updated']++;
634 }
635 }
636 }
637
638 private function knownCmsHref(string $href): string
639 {
640 $href = trim($href);
641 if ($href === '' || str_starts_with($href, '#')) {
642 return '';
643 }
644
645 $fragment = '';
646 if (str_contains($href, '#')) {
647 [$href, $fragmentValue] = explode('#', $href, 2);
648 $fragment = $fragmentValue !== '' ? '#' . $fragmentValue : '';
649 }
650
651 $query = parse_url($href, PHP_URL_QUERY);
652 if (is_string($query) && $query !== '') {
653 parse_str($query, $params);
654 $document = basename((string)($params['doc'] ?? ''));
655 $mapped = $this->referenceDocumentPermalink($document);
656 return $mapped !== '' ? $mapped . $fragment : '';
657 }
658
659 if (preg_match('#^https?://#i', $href)) {
660 return '';
661 }
662 $path = parse_url($href, PHP_URL_PATH);
663 $document = basename(is_string($path) ? $path : $href);
664 $mapped = $this->referenceDocumentPermalink($document);
665 return $mapped !== '' ? $mapped . $fragment : '';
666 }
667
668 private function upsertPage(string $language, array $data): int
669 {
670 $dd = dbxContentLng::ddContent($language);
671 $existing = $this->db->select1($dd, array('permalink' => (string)$data['permalink']), '*', 0);
672 $id = (int)($existing['id'] ?? 0);
673
674 if ($id > 0) {
675 $isImported = str_contains((string)($existing['keywords'] ?? ''), 'dbxdocs-migration');
676 if (!$this->force && $isImported) {
677 $this->result['pages_preserved']++;
678 return $id;
679 }
680 if (!$this->force && !$isImported) {
681 $navigation = array_intersect_key($data, array_flip(array('folder', 'menu_title', 'sorter')));
682 if ($this->db->update($dd, $navigation, $id, 0, 1, 0, 1) === 1) {
683 $this->result['pages_updated']++;
684 }
685 return $id;
686 }
687 if ($this->db->update($dd, $data, $id, 0, 1, 0, 1) !== 1) {
688 throw new RuntimeException('CMS-Seite konnte nicht aktualisiert werden: ' . (string)$data['permalink']);
689 }
690 $this->result['pages_updated']++;
691 } else {
692 if ($this->db->insert($dd, $data, 0, 1, 0, 1) !== 1) {
693 throw new RuntimeException('CMS-Seite konnte nicht angelegt werden: ' . (string)$data['permalink']);
694 }
695 $id = (int)$this->db->get_insert_id();
696 $this->result['pages_created']++;
697 }
698
699 dbxContentPermalinkIndex::upsertPage(
700 $id,
701 (string)$data['permalink'],
702 (string)($data['group_read'] ?? '*'),
703 (int)($data['activ'] ?? 1),
704 $language
705 );
706 return $id;
707 }
708
709 private function extractDoxygenContents(string $document): string
710 {
711 $file = $this->doxygenMigrationFile($document);
712 if (!is_file($file) || !is_readable($file)) {
713 throw new RuntimeException('Doxygen-Migrationsquelle fehlt.');
714 }
715
716 $html = file_get_contents($file);
717 if (!is_string($html) || $html === '') {
718 throw new RuntimeException('Doxygen-Migrationsquelle ist leer.');
719 }
720
721 $dom = new DOMDocument('1.0', 'UTF-8');
722 $previous = libxml_use_internal_errors(true);
723 $dom->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
724 libxml_clear_errors();
725 libxml_use_internal_errors($previous);
726
727 $xpath = new DOMXPath($dom);
728 $nodes = $xpath->query('//div[contains(concat(" ", normalize-space(@class), " "), " contents ")]');
729 $contents = $nodes !== false ? $nodes->item(0) : null;
730 if (!$contents instanceof DOMElement) {
731 throw new RuntimeException('Doxygen-Inhaltsbereich wurde nicht gefunden.');
732 }
733
734 foreach ($xpath->query('.//script|.//form', $contents) ?: array() as $unsafe) {
735 $unsafe->parentNode?->removeChild($unsafe);
736 }
737 foreach ($xpath->query('.//a[@href]', $contents) ?: array() as $link) {
738 if ($link instanceof DOMElement) {
739 $link->setAttribute('href', $this->rewriteReferenceLink($link->getAttribute('href')));
740 }
741 }
742 foreach ($xpath->query('.//img[@src]', $contents) ?: array() as $image) {
743 if (!$image instanceof DOMElement) {
744 continue;
745 }
746 $src = trim($image->getAttribute('src'));
747 if ($src !== '' && !preg_match('#^(?:https?:|data:|/)#i', $src)) {
748 $image->setAttribute('src', 'docs/assets/' . basename(parse_url($src, PHP_URL_PATH) ?: $src));
749 }
750 $image->setAttribute('loading', 'lazy');
751 }
752 $this->neutralizeInterpreterMarkers($dom, $xpath, $contents);
753
754 $out = '';
755 foreach ($contents->childNodes as $child) {
756 $out .= $dom->saveHTML($child);
757 }
758 return '<div class="dbxdocs-cms-article">' . trim($out) . '</div>';
759 }
760
761 private function doxygenMigrationFile(string $document): string
762 {
763 $document = basename($document);
764 $current = $this->referenceDir . '/' . $document;
765 if (is_file($current)) {
766 return $current;
767 }
768
769 $archives = glob($this->baseDir . '/reference/archive/manual-content-migration-*', GLOB_ONLYDIR) ?: array();
770 rsort($archives, SORT_NATURAL);
771 foreach ($archives as $archive) {
772 $candidate = rtrim(str_replace('\\', '/', $archive), '/') . '/' . $document;
773 if (is_file($candidate)) {
774 return $candidate;
775 }
776 }
777 return $current;
778 }
779
780 private function extractLegacyContents(string $relativeFile): string
781 {
782 $file = $this->baseDir . '/' . ltrim(str_replace('\\', '/', $relativeFile), '/');
783 if (!is_file($file) || !is_readable($file)) {
784 throw new RuntimeException('Dokumentationsquelle fehlt.');
785 }
786 $extension = strtolower((string)pathinfo($file, PATHINFO_EXTENSION));
787 $source = file_get_contents($file);
788 if (!is_string($source) || $source === '') {
789 throw new RuntimeException('Dokumentationsquelle ist leer.');
790 }
791 if ($extension === 'txt') {
792 return '<div class="dbxdocs-cms-article"><pre class="fragment">'
793 . htmlspecialchars($source, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
794 . '</pre></div>';
795 }
796
797 $dom = new DOMDocument('1.0', 'UTF-8');
798 $previous = libxml_use_internal_errors(true);
799 $dom->loadHTML('<?xml encoding="UTF-8">' . $source, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
800 libxml_clear_errors();
801 libxml_use_internal_errors($previous);
802 $xpath = new DOMXPath($dom);
803 $body = $xpath->query('//body')->item(0);
804 if (!$body instanceof DOMElement) {
805 throw new RuntimeException('HTML-Inhaltsbereich wurde nicht gefunden.');
806 }
807 foreach ($xpath->query('.//script|.//style|.//form', $body) ?: array() as $unsafe) {
808 $unsafe->parentNode?->removeChild($unsafe);
809 }
810 $this->neutralizeInterpreterMarkers($dom, $xpath, $body);
811 $out = '';
812 foreach ($body->childNodes as $child) {
813 $out .= $dom->saveHTML($child);
814 }
815 return '<div class="dbxdocs-cms-article">' . trim($out) . '</div>';
816 }
817
823 private function neutralizeInterpreterMarkers(
824 DOMDocument $dom,
825 DOMXPath $xpath,
826 DOMElement $root
827 ): void {
828 $query = './/*[contains(concat(" ", normalize-space(@class), " "), " fragment ")'
829 . ' or self::pre or self::code'
830 . ' or contains(concat(" ", normalize-space(@class), " "), " tt ")]//text()';
831 $nodes = $xpath->query($query, $root);
832 if ($nodes === false) {
833 return;
834 }
835
836 $textNodes = array();
837 foreach ($nodes as $node) {
838 $textNodes[] = $node;
839 }
840 foreach ($textNodes as $textNode) {
841 $text = (string)$textNode->nodeValue;
842 if (!str_contains($text, '[') && !str_contains($text, '{')) {
843 continue;
844 }
845 $parts = preg_split('/([\‍[\{])/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
846 foreach ($parts === false ? array($text) : $parts as $part) {
847 if ($part === '[' || $part === '{') {
848 $marker = $dom->createElement('span');
849 $marker->setAttribute('class', 'dbxdocs-code-punctuation');
850 $marker->appendChild($dom->createTextNode($part));
851 $textNode->parentNode?->insertBefore($marker, $textNode);
852 } elseif ($part !== '') {
853 $textNode->parentNode?->insertBefore($dom->createTextNode($part), $textNode);
854 }
855 }
856 $textNode->parentNode?->removeChild($textNode);
857 }
858 }
859
860 private function rewriteReferenceLink(string $href): string
861 {
862 $href = trim($href);
863 if ($href === '' || str_starts_with($href, '#') || preg_match('#^(?:https?:|mailto:|tel:|/)#i', $href)) {
864 return $href;
865 }
866
867 $fragment = '';
868 if (str_contains($href, '#')) {
869 [$href, $fragment] = explode('#', $href, 2);
870 $fragment = $fragment !== '' ? '#' . $fragment : '';
871 }
872 $path = parse_url($href, PHP_URL_PATH);
873 $document = basename(is_string($path) ? $path : $href);
874 if (!str_ends_with(strtolower($document), '.html')) {
875 return $href . $fragment;
876 }
877
878 $mapped = $this->referenceDocumentPermalink($document);
879 if ($mapped !== '') {
880 return ltrim($mapped, '/') . $fragment;
881 }
882
883 return '?dbx_modul=dbxDocs&dbx_run1=reference&dbx_run2=page&doc='
884 . rawurlencode($document)
885 . $fragment;
886 }
887
888 private function referenceDocumentPermalink(string $document): string
889 {
890 $document = strtolower(basename(trim($document)));
891 if ($document === '') {
892 return '';
893 }
894
895 $mapped = $this->htmlToPermalink[$document] ?? '';
896 if ($mapped !== '') {
897 return $mapped;
898 }
899
900 if (preg_match('/^dbxcontent_tutorial_(.+)\.html$/', $document, $match) === 1) {
901 $slug = str_replace('_', '-', (string)$match[1]);
902 return str_starts_with($slug, 'tutorial-') || str_starts_with($slug, 'tutorials-')
903 ? $slug
904 : '';
905 }
906 return '';
907 }
908}
if(!is_array( $before)||(int)( $before[ 'id'] ?? 0) !==$mediaId) if((string)($before['media_type'] ?? '') !=='video') $updated
Einmalige, wiederholbare Übernahme der redaktionellen Dokumentation in dbxContent.
__construct(object $db, string $baseDir, bool $force=false)
dbx()
Liefert die zentrale dbXapp-API als Singleton.
Definition dbxApi.php:2805
esc($value)
Escaped einen Wert fuer HTML-Text und HTML-Attribute.
Definition dbxApi.php:2310
parse_url($data)
Zerlegt URL-/Parameterdaten in einen Array.
Definition dbxApi.php:2416
if(!is_object( $db)) if($db->connect_db_server($server) !==1) $created
if($cinematic===''||!str_contains($cinematic, 'data-dbx-cinema')) $page