25 private string $baseDir;
26 private string $referenceDir;
28 private array $folderIds = array();
29 private array $htmlToPermalink = array();
30 private array $result = array(
31 'folders_created' => 0,
32 'folders_updated' => 0,
35 'pages_preserved' => 0,
40 public function __construct(
object $db,
string $baseDir,
bool $force =
false)
43 $this->baseDir = rtrim(str_replace(
'\\',
'/', $baseDir),
'/');
44 $this->referenceDir = $this->baseDir .
'/reference/current';
45 $this->force = $force;
48 public function run(): array
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());
63 private function folderPlan(): 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'),
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'),
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'),
96 private function pagePlan(): 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'),
148 private function legacyPlan(): 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'),
167 private function prepareHtmlMap(): void
169 foreach ($this->pagePlan() as
$page) {
170 $this->htmlToPermalink[strtolower((
string)
$page[
'html'])] = (string)
$page[
'permalink'];
174 private function prepareFolderTree(): void
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(
182 (
string)$folder[
'name'],
184 (
string)$folder[
'sorter']
186 $this->folderIds[$language][(string)$folder[
'key']] = $id;
191 private function ensureFolder(
string $language,
string $name,
int $parentId,
string $sorter): int
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);
202 'parent_id' => $parentId,
207 throw new RuntimeException(
'Ordner konnte nicht angelegt werden: ' . $language .
'/' . $name);
209 $this->
result[
'folders_created']++;
210 return (
int)$this->db->get_insert_id();
213 private function moveTutorialFolders(): void
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);
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) {
228 $pages = $this->db->select(
229 dbxContentLng::ddContent($language),
230 array(
'folder' => $id),
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,
250 $this->folderIds[$language][
'tutorials'] = $id;
251 $this->
result[
'folders_updated']++;
258 private function folderIsEmpty(
string $language,
int $folderId): bool
260 if ($folderId <= 0) {
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());
268 private function shortenTutorialTitles(): void
270 foreach (array(
'de',
'en',
'es') as $language) {
271 $folderId = (int)($this->folderIds[$language][
'tutorials'] ?? 0);
272 if ($folderId <= 0) {
275 $dd = dbxContentLng::ddContent($language);
276 $rows = $this->db->select(
278 array(
'folder' => $folderId),
279 'id,title,menu_title,seo_title',
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'] ??
''));
293 $menuTitle = $this->shortTutorialTitle((
string)($row[
'title'] ??
''));
294 if ($id <= 0 || $menuTitle ===
'') {
297 if (!$this->force && trim((
string)($row[
'menu_title'] ??
'')) !==
'') {
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']++;
311 private function shortTutorialTitle(
string $title): string
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',
324 $title = $replacements[$title] ?? $title;
325 if (mb_strlen($title) <= 48) {
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);
333 return rtrim($short,
" \t\n\r\0\x0B,.;:-") .
'…';
336 private function provisionGermanDocumentation(): void
338 $sorterByFolder = array();
339 foreach ($this->pagePlan() as
$page) {
340 $folderKey = (string)
$page[
'folder'];
341 $sorterByFolder[$folderKey] = ($sorterByFolder[$folderKey] ?? 0) + 10;
343 $content = $this->extractDoxygenContents((
string)
$page[
'html']);
344 $this->upsertPage(
'de', array(
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',
354 'sorter' => sprintf(
'%04d', $sorterByFolder[$folderKey]),
355 'template' =>
'parent',
358 }
catch (\Throwable $exception) {
359 $this->
result[
'errors'][] = (string)
$page[
'html'] .
': ' . $exception->getMessage();
364 private function provisionLegacyDocumentation(): void
366 $sorterByFolder = array();
367 foreach ($this->legacyPlan() as
$page) {
368 $folderKey = (string)
$page[
'folder'];
369 $sorterByFolder[$folderKey] = ($sorterByFolder[$folderKey] ?? 0) + 10;
372 ? $this->extractDoxygenContents((
string)
$page[
'reference'])
373 : $this->extractLegacyContents((string)
$page[
'file']);
374 $this->upsertPage(
'de', array(
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',
384 'sorter' => sprintf(
'%04d', $sorterByFolder[$folderKey]),
385 'template' =>
'parent',
388 }
catch (\Throwable $exception) {
389 $this->
result[
'errors'][] = (string)
$page[
'file'] .
': ' . $exception->getMessage();
394 private function provisionServicePages(): void
396 $imprintDe = $this->db->select1(
397 dbxContentLng::ddContent(
'de'),
398 array(
'permalink' =>
'impressum'),
402 $imprintUid = trim((
string)($imprintDe[
'lng_uid'] ??
''));
403 $this->movePageToService(
'de', (
int)($imprintDe[
'id'] ?? 0),
'Impressum',
'0080');
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),
413 $this->movePageToService($language, (
int)($row[
'id'] ?? 0), $menuTitle,
'0080');
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'),
422 foreach ($contactPages as $language => $labels) {
423 $this->upsertPage($language, array(
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',
433 'template' =>
'parent',
434 'content' =>
'<p class="lead">' .
dbx()->
esc($labels[
'description']) .
'</p>'
435 .
'[modul=dbxContact]dbx_run1=form[/modul]',
440 private function movePageToService(
string $language,
int $id,
string $menuTitle,
string $sorter): void
445 $updated = $this->db->update(dbxContentLng::ddContent($language), array(
446 'folder' => (
int)($this->folderIds[$language][
'service'] ?? 0),
447 'menu_title' => $menuTitle,
451 $this->
result[
'pages_updated']++;
461 private function provisionCuratedDocumentationUpdates(): void
464 'dokumentation-installation' => array(
465 'revision' =>
'2026-08-01-installation-2',
466 'file' => dirname(__DIR__) .
'/content/dbxapp_user_installation.html',
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',
478 'template' =>
'parent',
481 'tutorial-installation' => array(
482 'revision' =>
'2026-08-01-tutorial-installation-2',
483 'file' => dirname(__DIR__) .
'/content/tutorial_installation.html',
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',
495 'template' =>
'parent',
498 'dokumentation-selbsttest' => array(
499 'revision' =>
'2026-08-01-selftest-2',
500 'file' => dirname(__DIR__) .
'/content/dbxapp_user_selftest.html',
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',
512 'template' =>
'parent',
515 'dokumentation-ki-design' => array(
516 'revision' =>
'2026-07-28-ki-design-2',
517 'file' => dirname(__DIR__) .
'/content/dbxapp_user_ki_design.html',
521 $dd = dbxContentLng::ddContent(
'de');
522 foreach ($updates as $permalink => $update) {
523 $file = (string)$update[
'file'];
526 $this->
result[
'errors'][] = basename(
$file) .
': Kuratierter CMS-Inhalt fehlt.';
530 $row = $this->db->select1(
532 array(
'permalink' => $permalink),
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 .
'"')) {
543 $page = is_array($update[
'page'] ??
null) ? $update[
'page'] : array();
545 if (
$page === array()) {
546 $this->
result[
'errors'][] = $permalink .
': Metadaten für die neue CMS-Seite fehlen.';
549 $folderKey = (string)(
$page[
'folder_key'] ??
'');
550 unset(
$page[
'folder_key']);
551 $page[
'folder'] = (int)($this->folderIds[
'de'][$folderKey] ?? 0);
554 $this->upsertPage(
'de',
$page);
555 }
catch (\Throwable $exception) {
556 $this->
result[
'errors'][] = $permalink .
': ' . $exception->getMessage();
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);
568 if ($this->db->update(
$dd, $changes, $id, 0, 1, 0, 1) === 1) {
569 $this->
result[
'pages_updated']++;
570 dbxContentPermalinkIndex::upsertPage(
573 (
string)($changes[
'group_read'] ??
'*'),
574 (
int)($changes[
'activ'] ?? 1),
587 private function repairDocumentationLinks(): void
589 $dd = dbxContentLng::ddContent(
'de');
590 $rows = $this->db->select(
593 'id,permalink,content',
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'] ??
'');
609 || (!str_starts_with($permalink,
'dokumentation-') && !str_starts_with($permalink,
'tutorial-'))
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];
622 return 'href=' . (string)$match[1]
623 . htmlspecialchars($mapped, ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8')
632 if ($this->db->update(
$dd, array(
'content' =>
$updated), $id, 0, 1, 0, 1) === 1) {
633 $this->
result[
'links_updated']++;
638 private function knownCmsHref(
string $href): string
641 if ($href ===
'' || str_starts_with($href,
'#')) {
646 if (str_contains($href,
'#')) {
647 [$href, $fragmentValue] = explode(
'#', $href, 2);
648 $fragment = $fragmentValue !==
'' ?
'#' . $fragmentValue :
'';
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 :
'';
659 if (preg_match(
'#^https?://#i', $href)) {
663 $document = basename(is_string($path) ? $path : $href);
664 $mapped = $this->referenceDocumentPermalink($document);
665 return $mapped !==
'' ? $mapped . $fragment :
'';
668 private function upsertPage(
string $language, array $data): int
670 $dd = dbxContentLng::ddContent($language);
671 $existing = $this->db->select1(
$dd, array(
'permalink' => (
string)$data[
'permalink']),
'*', 0);
672 $id = (int)($existing[
'id'] ?? 0);
675 $isImported = str_contains((
string)($existing[
'keywords'] ??
''),
'dbxdocs-migration');
676 if (!$this->force && $isImported) {
677 $this->
result[
'pages_preserved']++;
680 if (!$this->force && !$isImported) {
681 $navigation = array_intersect_key($data, array_flip(array(
'folder',
'menu_title',
'sorter')));
683 $this->
result[
'pages_updated']++;
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']);
690 $this->
result[
'pages_updated']++;
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']);
695 $id = (int)$this->db->get_insert_id();
696 $this->
result[
'pages_created']++;
699 dbxContentPermalinkIndex::upsertPage(
701 (
string)$data[
'permalink'],
702 (
string)($data[
'group_read'] ??
'*'),
703 (
int)($data[
'activ'] ?? 1),
709 private function extractDoxygenContents(
string $document): string
711 $file = $this->doxygenMigrationFile($document);
713 throw new RuntimeException(
'Doxygen-Migrationsquelle fehlt.');
718 throw new RuntimeException(
'Doxygen-Migrationsquelle ist leer.');
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);
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.');
734 foreach ($xpath->query(
'.//script|.//form', $contents) ?: array() as $unsafe) {
735 $unsafe->parentNode?->removeChild($unsafe);
737 foreach ($xpath->query(
'.//a[@href]', $contents) ?: array() as $link) {
738 if ($link instanceof DOMElement) {
739 $link->setAttribute(
'href', $this->rewriteReferenceLink($link->getAttribute(
'href')));
742 foreach ($xpath->query(
'.//img[@src]', $contents) ?: array() as $image) {
743 if (!$image instanceof DOMElement) {
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));
750 $image->setAttribute(
'loading',
'lazy');
752 $this->neutralizeInterpreterMarkers($dom, $xpath, $contents);
755 foreach ($contents->childNodes as $child) {
756 $out .= $dom->saveHTML($child);
758 return '<div class="dbxdocs-cms-article">' . trim(
$out) .
'</div>';
761 private function doxygenMigrationFile(
string $document): string
763 $document = basename($document);
764 $current = $this->referenceDir .
'/' . $document;
765 if (is_file($current)) {
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)) {
780 private function extractLegacyContents(
string $relativeFile): string
782 $file = $this->baseDir .
'/' . ltrim(str_replace(
'\\',
'/', $relativeFile),
'/');
784 throw new RuntimeException(
'Dokumentationsquelle fehlt.');
786 $extension = strtolower((
string)pathinfo(
$file, PATHINFO_EXTENSION));
789 throw new RuntimeException(
'Dokumentationsquelle ist leer.');
791 if ($extension ===
'txt') {
792 return '<div class="dbxdocs-cms-article"><pre class="fragment">'
793 . htmlspecialchars(
$source, ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8')
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.');
807 foreach ($xpath->query(
'.//script|.//style|.//form',
$body) ?: array() as $unsafe) {
808 $unsafe->parentNode?->removeChild($unsafe);
810 $this->neutralizeInterpreterMarkers($dom, $xpath,
$body);
812 foreach (
$body->childNodes as $child) {
813 $out .= $dom->saveHTML($child);
815 return '<div class="dbxdocs-cms-article">' . trim(
$out) .
'</div>';
823 private function neutralizeInterpreterMarkers(
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) {
836 $textNodes = array();
837 foreach ($nodes as $node) {
838 $textNodes[] = $node;
840 foreach ($textNodes as $textNode) {
841 $text = (string)$textNode->nodeValue;
842 if (!str_contains($text,
'[') && !str_contains($text,
'{')) {
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);
856 $textNode->parentNode?->removeChild($textNode);
860 private function rewriteReferenceLink(
string $href): string
863 if ($href ===
'' || str_starts_with($href,
'#') || preg_match(
'#^(?:https?:|mailto:|tel:|/)#i', $href)) {
868 if (str_contains($href,
'#')) {
869 [$href, $fragment] = explode(
'#', $href, 2);
870 $fragment = $fragment !==
'' ?
'#' . $fragment :
'';
873 $document = basename(is_string($path) ? $path : $href);
874 if (!str_ends_with(strtolower($document),
'.html')) {
875 return $href . $fragment;
878 $mapped = $this->referenceDocumentPermalink($document);
879 if ($mapped !==
'') {
880 return ltrim($mapped,
'/') . $fragment;
883 return '?dbx_modul=dbxDocs&dbx_run1=reference&dbx_run2=page&doc='
884 . rawurlencode($document)
888 private function referenceDocumentPermalink(
string $document): string
890 $document = strtolower(basename(trim($document)));
891 if ($document ===
'') {
895 $mapped = $this->htmlToPermalink[$document] ??
'';
896 if ($mapped !==
'') {
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-')