diff --git a/application/config/cms.php b/application/config/cms.php
new file mode 100644
index 000000000..c703c9c58
--- /dev/null
+++ b/application/config/cms.php
@@ -0,0 +1,8 @@
+ 'basis/cms:r']);
+ $this->loadPhrases(['global', 'ui', 'cms']);
+ }
+
+ public function index()
+ {
+ $this->load->view('Cms.php');
+ }
+}
diff --git a/application/controllers/api/frontend/v1/CmsAdmin.php b/application/controllers/api/frontend/v1/CmsAdmin.php
new file mode 100644
index 000000000..d6a15d6ea
--- /dev/null
+++ b/application/controllers/api/frontend/v1/CmsAdmin.php
@@ -0,0 +1,472 @@
+ ['basis/cms:r'],
+ 'getContent' => ['basis/cms:r'],
+ 'getContentsprache' => ['basis/cms:r'],
+ 'getTemplates' => ['basis/cms:r'],
+ 'getOrganisationseinheiten' => ['basis/cms:r'],
+ 'getSprachen' => ['basis/cms:r'],
+ 'getUsage' => ['basis/cms:r'],
+ 'postContent' => ['basis/cms:rw'],
+ 'postTranslation' => ['basis/cms:rw'],
+ 'postVersion' => ['basis/cms:rw'],
+ 'putProperties' => ['basis/cms:rw'],
+ 'deleteContent' => ['basis/cms:rw'],
+ 'deleteContentsprache' => ['basis/cms:rw']
+ ]);
+
+ $this->load->library('CmsAdminLib');
+ $this->load->library('PermissionLib');
+ $this->load->model('content/Content_model', 'ContentModel');
+ $this->load->model('content/Contentsprache_model', 'ContentspracheModel');
+ $this->load->model('content/Contentchild_model', 'ContentchildModel');
+ $this->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
+ $this->load->model('content/Template_model', 'TemplateModel');
+
+ $this->loadPhrases(['global', 'cms']);
+ }
+
+ public function getTree()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('menu', 'Menu', 'required|in_list[content,news]');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $menu = $this->input->get('menu', TRUE);
+ $filter = $this->input->get('filter', TRUE);
+
+ $entitledResult = $this->cmsadminlib->getEntitledOe();
+ $entitledOe = isError($entitledResult) ? [] : getData($entitledResult);
+
+ $nodes = [];
+
+ if ($menu === 'news')
+ {
+ $newsResult = $this->ContentModel->getNewsContent();
+ if (!isError($newsResult) && getData($newsResult))
+ {
+ foreach (getData($newsResult) as $row)
+ {
+ $titel = '';
+ $csResult = $this->ContentspracheModel->getOne(
+ (int) $row->content_id, DEFAULT_LANGUAGE
+ );
+ if (!isError($csResult))
+ $titel = getData($csResult)->titel;
+
+ $node = new stdClass();
+ $node->content_id = (int) $row->content_id;
+ $node->titel = mb_substr($titel, 0, 15) . ' '
+ . date('d.m.Y', strtotime($row->insertamum));
+ $node->template_kurzbz = $row->template_kurzbz;
+ $node->oe_kurzbz = $row->oe_kurzbz;
+ $node->aktiv = ($row->aktiv === 't' || $row->aktiv === true);
+ $node->entitled = false;
+ $node->groups = [];
+ $node->children = [];
+ $nodes[] = $node;
+ }
+ }
+ }
+ else
+ {
+ if (!empty($filter))
+ {
+ $clean = str_replace(['!', '.', '?', ','], '', $filter);
+ $searchItems = preg_split('/\s+/', $clean, -1, PREG_SPLIT_NO_EMPTY);
+ $rootsResult = $this->ContentModel->searchCms($searchItems);
+ }
+ else
+ {
+ $rootsResult = $this->ContentModel->getRootContent();
+ }
+
+ if (!isError($rootsResult) && getData($rootsResult))
+ {
+ $visited = [];
+ foreach (getData($rootsResult) as $row)
+ {
+ $node = $this->buildTreeNode(
+ (int) $row->content_id, 0, $visited
+ );
+ if ($node !== null)
+ $nodes[] = $node;
+ }
+ }
+ }
+
+ $allIds = [];
+ $this->collectContentIds($nodes, $allIds);
+
+ $groupMap = [];
+ if (!empty($allIds))
+ {
+ $gResult = $this->ContentgruppeModel->getGruppenForContents($allIds);
+ if (!isError($gResult))
+ $groupMap = getData($gResult);
+ }
+
+ $this->applyMeta($nodes, $entitledOe, $groupMap);
+
+ $result = [];
+ foreach ($nodes as $node)
+ {
+ $pruned = $this->pruneUnentitled($node);
+ if ($pruned !== null)
+ $result[] = $pruned;
+ }
+
+ $this->terminateWithSuccess($result);
+ }
+
+ public function getContent()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+
+ $entitledResult = $this->cmsadminlib->isEntitledForContent($content_id);
+ $entitled = $this->getDataOrTerminateWithError($entitledResult);
+
+ if (!$entitled)
+ $this->terminateWithError('cms/keineBerechtigungFuerDiesenEintrag');
+
+ $this->ContentModel->resetQuery();
+ $contentResult = $this->ContentModel->load($content_id);
+ $contentData = $this->getDataOrTerminateWithError($contentResult);
+ if (empty($contentData))
+ $this->terminateWithError('cms/contentNichtGefunden');
+ $content = $contentData[0];
+
+ $langResult = $this->ContentspracheModel->getLanguages($content_id);
+ $languages = $this->getDataOrTerminateWithError($langResult);
+
+ $versions = new stdClass();
+ foreach ($languages as $sprache)
+ {
+ $vResult = $this->ContentspracheModel->getVersions($content_id, $sprache);
+ if (!isError($vResult) && getData($vResult))
+ {
+ $versions->$sprache = array_map(function ($v) {
+ return (int) $v->version;
+ }, getData($vResult));
+ }
+ }
+
+ $result = new stdClass();
+ $result->content_id = (int) $content->content_id;
+ $result->template_kurzbz = $content->template_kurzbz;
+ $result->oe_kurzbz = $content->oe_kurzbz;
+ $result->aktiv = ($content->aktiv === 't' || $content->aktiv === true);
+ $result->menu_open = ($content->menu_open === 't' || $content->menu_open === true);
+ $result->beschreibung = $content->beschreibung ?: '';
+ $result->entitled = true;
+ $result->languages = $languages;
+ $result->versions = $versions;
+
+ $this->terminateWithSuccess($result);
+ }
+
+ public function getContentsprache()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ $this->form_validation->set_rules('version', 'Version', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+ $sprache = $this->input->get('sprache', TRUE);
+ $version = $this->input->get('version', TRUE);
+
+ $result = $this->ContentspracheModel->getOne($content_id, $sprache, $version);
+ if (isError($result))
+ $this->terminateWithError('cms/versionNichtGefunden');
+
+ $data = getData($result);
+ $data->sichtbar = ($data->sichtbar === 't' || $data->sichtbar === true);
+
+ $this->terminateWithSuccess($data);
+ }
+
+ public function getTemplates()
+ {
+ $this->TemplateModel->addSelect('template_kurzbz, bezeichnung');
+ $this->TemplateModel->addOrder('bezeichnung');
+ $result = $this->TemplateModel->loadWhere([]);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function getOrganisationseinheiten()
+ {
+ $this->load->model(
+ 'organisation/Organisationseinheit_model', 'OrganisationseinheitModel'
+ );
+ $this->OrganisationseinheitModel->addSelect(
+ 'oe_kurzbz, bezeichnung, organisationseinheittyp_kurzbz, aktiv'
+ );
+ $this->OrganisationseinheitModel->addOrder('bezeichnung');
+ $result = $this->OrganisationseinheitModel->loadWhere([]);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function getSprachen()
+ {
+ $this->load->model('system/Sprache_model', 'SpracheModel');
+ $this->SpracheModel->addSelect('sprache, bezeichnung');
+ $result = $this->SpracheModel->loadWhere([]);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function getUsage()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+ $result = $this->ContentModel->getUsage($content_id);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function postContent()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('parent_content_id', 'Parent', 'is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $parent = $this->input->post('parent_content_id');
+ if (empty($parent))
+ $parent = null;
+
+ $result = $this->cmsadminlib->createContent($parent);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function postTranslation()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ $this->form_validation->set_rules('version', 'Version', 'required|is_natural');
+ $this->form_validation->set_rules('target_sprache', 'Target Sprache', 'required');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $result = $this->cmsadminlib->createTranslation(
+ $this->input->post('content_id'),
+ $this->input->post('sprache'),
+ $this->input->post('version'),
+ $this->input->post('target_sprache')
+ );
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function postVersion()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $result = $this->cmsadminlib->createVersion(
+ $this->input->post('content_id'),
+ $this->input->post('sprache')
+ );
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function putProperties()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ $this->form_validation->set_rules('version', 'Version', 'required|is_natural');
+ $this->form_validation->set_rules('template_kurzbz', 'Template', 'required');
+ $this->form_validation->set_rules('oe_kurzbz', 'OE', 'required');
+ $this->form_validation->set_rules('titel', 'Titel', 'required|max_length[256]');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $daten = [
+ 'content_id' => $this->input->post('content_id'),
+ 'sprache' => $this->input->post('sprache'),
+ 'version' => $this->input->post('version'),
+ 'template_kurzbz' => $this->input->post('template_kurzbz'),
+ 'oe_kurzbz' => $this->input->post('oe_kurzbz'),
+ 'titel' => $this->input->post('titel'),
+ 'beschreibung' => $this->input->post('beschreibung') ?: '',
+ 'aktiv' => filter_var($this->input->post('aktiv'), FILTER_VALIDATE_BOOLEAN),
+ 'menu_open' => filter_var($this->input->post('menu_open'), FILTER_VALIDATE_BOOLEAN),
+ 'sichtbar' => filter_var($this->input->post('sichtbar'), FILTER_VALIDATE_BOOLEAN)
+ ];
+
+ $result = $this->cmsadminlib->saveProperties($daten);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function deleteContent()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'd'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $result = $this->cmsadminlib->deleteContent($this->input->post('content_id'));
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function deleteContentsprache()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'd'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->validateContentSpracheVersion();
+
+ $result = $this->cmsadminlib->deleteVersion(
+ $this->input->post('content_id'),
+ $this->input->post('sprache'),
+ $this->input->post('version')
+ );
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ // --- Private helpers ---
+
+ private function validateContentSpracheVersion()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ $this->form_validation->set_rules('version', 'Version', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+ }
+
+ private function buildTreeNode($content_id, $depth, &$visited)
+ {
+ if ($depth > self::MAX_TREE_DEPTH || isset($visited[$content_id]))
+ return null;
+
+ $visited[$content_id] = true;
+
+ $this->ContentModel->resetQuery();
+ $contentResult = $this->ContentModel->load($content_id);
+ if (isError($contentResult) || empty(getData($contentResult)))
+ return null;
+ $content = getData($contentResult)[0];
+
+ $titel = '';
+ $csResult = $this->ContentspracheModel->getOne($content_id, DEFAULT_LANGUAGE);
+ if (!isError($csResult))
+ $titel = getData($csResult)->titel;
+
+ $childrenResult = $this->ContentchildModel->getChilds($content_id, DEFAULT_LANGUAGE);
+ $children = [];
+ if (!isError($childrenResult) && getData($childrenResult))
+ {
+ foreach (getData($childrenResult) as $child)
+ {
+ $childNode = $this->buildTreeNode(
+ (int) $child->child_content_id, $depth + 1, $visited
+ );
+ if ($childNode !== null)
+ $children[] = $childNode;
+ }
+ }
+
+ $node = new stdClass();
+ $node->content_id = (int) $content_id;
+ $node->titel = $titel;
+ $node->template_kurzbz = $content->template_kurzbz;
+ $node->oe_kurzbz = $content->oe_kurzbz;
+ $node->aktiv = ($content->aktiv === 't' || $content->aktiv === true);
+ $node->entitled = false;
+ $node->groups = [];
+ $node->children = $children;
+
+ return $node;
+ }
+
+ private function collectContentIds($nodes, &$ids)
+ {
+ foreach ($nodes as $node)
+ {
+ $ids[] = $node->content_id;
+ if (!empty($node->children))
+ $this->collectContentIds($node->children, $ids);
+ }
+ }
+
+ private function applyMeta(&$nodes, $entitledOe, $groupMap)
+ {
+ foreach ($nodes as $node)
+ {
+ $node->entitled = in_array($node->oe_kurzbz, $entitledOe);
+ $node->groups = isset($groupMap[$node->content_id])
+ ? $groupMap[$node->content_id]
+ : [];
+ if (!empty($node->children))
+ $this->applyMeta($node->children, $entitledOe, $groupMap);
+ }
+ }
+
+ private function pruneUnentitled($node)
+ {
+ $kept = [];
+ foreach ($node->children as $child)
+ {
+ $pruned = $this->pruneUnentitled($child);
+ if ($pruned !== null)
+ $kept[] = $pruned;
+ }
+ $node->children = $kept;
+
+ if ($node->entitled || !empty($node->children))
+ return $node;
+
+ return null;
+ }
+}
diff --git a/application/controllers/api/frontend/v1/CmsAdminInhalt.php b/application/controllers/api/frontend/v1/CmsAdminInhalt.php
new file mode 100644
index 000000000..ceee66633
--- /dev/null
+++ b/application/controllers/api/frontend/v1/CmsAdminInhalt.php
@@ -0,0 +1,193 @@
+ ['basis/cms:r'],
+ 'getFormData' => ['basis/cms:r'],
+ 'getLock' => ['basis/cms:r'],
+ 'getVersions' => ['basis/cms:r'],
+ 'putFormData' => ['basis/cms:rw'],
+ 'postLock' => ['basis/cms:rw'],
+ 'deleteLock' => ['basis/cms:rw'],
+ 'deleteLockForced' => ['basis/cms:rw']
+ ]);
+
+ $this->load->library('CmsAdminLib');
+ $this->load->library('XsdSchemaLib');
+ $this->load->library('PermissionLib');
+ $this->load->model('content/Contentsprache_model', 'ContentspracheModel');
+ $this->load->model('content/Template_model', 'TemplateModel');
+
+ $this->loadPhrases(['global', 'cms']);
+ }
+
+ public function getFormSchema()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('template_kurzbz', 'Template', 'required');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $template_kurzbz = $this->input->get('template_kurzbz', TRUE);
+
+ $this->TemplateModel->resetQuery();
+ $templateResult = $this->TemplateModel->load($template_kurzbz);
+ $templateData = $this->getDataOrTerminateWithError($templateResult);
+ if (empty($templateData))
+ $this->terminateWithError('cms/vorlageNichtGefunden');
+
+ $schemaResult = $this->xsdschemalib->parseSchema(
+ $templateData[0]->xsd, $template_kurzbz
+ );
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($schemaResult));
+ }
+
+ public function getFormData()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ $this->form_validation->set_rules('version', 'Version', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+ $sprache = $this->input->get('sprache', TRUE);
+ $version = $this->input->get('version', TRUE);
+
+ $versionResult = $this->ContentspracheModel->getOne($content_id, $sprache, $version);
+ $row = $this->getDataOrTerminateWithError($versionResult);
+
+ $this->load->model('content/Content_model', 'ContentModel');
+ $this->ContentModel->resetQuery();
+ $contentResult = $this->ContentModel->load($content_id);
+ $contentData = $this->getDataOrTerminateWithError($contentResult);
+ if (empty($contentData))
+ $this->terminateWithError('cms/contentNichtGefunden');
+
+ $this->TemplateModel->resetQuery();
+ $templateResult = $this->TemplateModel->load($contentData[0]->template_kurzbz);
+ $templateData = $this->getDataOrTerminateWithError($templateResult);
+ if (empty($templateData))
+ $this->terminateWithError('cms/vorlageNichtGefunden');
+
+ $schemaResult = $this->xsdschemalib->parseSchema(
+ $templateData[0]->xsd, $contentData[0]->template_kurzbz
+ );
+ $schema = $this->getDataOrTerminateWithError($schemaResult);
+
+ $valuesResult = $this->xsdschemalib->extractValues($row->content, $schema);
+ $values = $this->getDataOrTerminateWithError($valuesResult);
+
+ $lockResult = $this->cmsadminlib->getLockState($content_id, $sprache, $version);
+ $lock = $this->getDataOrTerminateWithError($lockResult);
+
+ $this->terminateWithSuccess([
+ 'schema' => $schema,
+ 'values' => $values,
+ 'sperre' => $lock
+ ]);
+ }
+
+ public function getLock()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ $this->form_validation->set_rules('version', 'Version', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+ $sprache = $this->input->get('sprache', TRUE);
+ $version = $this->input->get('version', TRUE);
+
+ $result = $this->cmsadminlib->getLockState($content_id, $sprache, $version);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function getVersions()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+ $sprache = $this->input->get('sprache', TRUE);
+
+ $result = $this->ContentspracheModel->getVersions($content_id, $sprache);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function putFormData()
+ {
+ // LEGACY-QUIRK: the legacy XSDFormPrinter_XML branch in admin.php checks neither the
+ // permission type nor the lock. Kept as a functional copy. See Q3 in the contract.
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('sprache', 'Sprache', 'required');
+ $this->form_validation->set_rules('version', 'Version', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->post('content_id');
+ $sprache = $this->input->post('sprache');
+ $version = $this->input->post('version');
+ $values = $this->input->post('values');
+
+ if (is_string($values))
+ $values = json_decode($values, true);
+
+ if (!is_array($values))
+ $values = [];
+
+ $result = $this->cmsadminlib->saveContentXml($content_id, $sprache, $version, $values);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function postLock()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
+ $this->terminateWithError('cms/keineBerechtigungFuerDieseAktion');
+
+ $contentsprache_id = $this->input->post('contentsprache_id');
+
+ $result = $this->cmsadminlib->lock($contentsprache_id);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function deleteLock()
+ {
+ // LEGACY-QUIRK: releaseOwnLocks releases all locks of the user. This method ignores
+ // the given contentsprache_id on purpose. The parameter stays in the API contract, so
+ // that a later fix does not change the interface. See Q1 in the contract.
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
+ $this->terminateWithError('cms/keineBerechtigungFuerDieseAktion');
+
+ $result = $this->cmsadminlib->releaseOwnLocks();
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function deleteLockForced()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms_sperrfreigabe', 'su'))
+ $this->terminateWithError('cms/keineBerechtigungFuerDieseAktion');
+
+ $contentsprache_id = $this->input->post('contentsprache_id');
+
+ $result = $this->cmsadminlib->forceRelease($contentsprache_id);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+}
diff --git a/application/controllers/api/frontend/v1/CmsAdminStruktur.php b/application/controllers/api/frontend/v1/CmsAdminStruktur.php
new file mode 100644
index 000000000..4d3421934
--- /dev/null
+++ b/application/controllers/api/frontend/v1/CmsAdminStruktur.php
@@ -0,0 +1,200 @@
+ ['basis/cms:r'],
+ 'getAllGruppen' => ['basis/cms:r'],
+ 'getChilds' => ['basis/cms:r'],
+ 'getPossibleChilds' => ['basis/cms:r'],
+ 'postGruppe' => ['basis/cms:rw'],
+ 'deleteGruppe' => ['basis/cms:rw'],
+ 'postChild' => ['basis/cms:rw'],
+ 'deleteChild' => ['basis/cms:rw'],
+ 'putChildSort' => ['basis/cms:rw']
+ ]);
+
+ $this->load->library('PermissionLib');
+ $this->load->model('content/Content_model', 'ContentModel');
+ $this->load->model('content/Contentchild_model', 'ContentchildModel');
+ $this->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
+
+ $this->loadPhrases(['global', 'cms']);
+ }
+
+ public function getGruppen()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+
+ $result = $this->ContentgruppeModel->getGruppen($content_id);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function getAllGruppen()
+ {
+ $result = $this->ContentgruppeModel->getAllGruppen();
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function getChilds()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+ $sprache = $this->input->get('sprache', TRUE);
+
+ if (empty($sprache))
+ $sprache = DEFAULT_LANGUAGE;
+
+ $result = $this->ContentchildModel->getChilds($content_id, $sprache);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function getPossibleChilds()
+ {
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_GET);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->get('content_id', TRUE);
+ $sprache = $this->input->get('sprache', TRUE);
+
+ if (empty($sprache))
+ $sprache = DEFAULT_LANGUAGE;
+
+ $result = $this->ContentModel->getPossibleChilds($content_id, $sprache);
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+
+ public function postGruppe()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('gruppe_kurzbz', 'Gruppe', 'required');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $result = $this->ContentgruppeModel->insert([
+ 'content_id' => $this->input->post('content_id'),
+ 'gruppe_kurzbz' => $this->input->post('gruppe_kurzbz'),
+ 'insertamum' => date('Y-m-d H:i:s'),
+ 'insertvon' => getAuthUID()
+ ]);
+
+ if (isError($result))
+ {
+ $this->terminateWithError('cms/gruppeBereitsZugeordnet');
+ }
+
+ $this->terminateWithSuccess(true);
+ }
+
+ public function deleteGruppe()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('gruppe_kurzbz', 'Gruppe', 'required');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $result = $this->ContentgruppeModel->delete([
+ $this->input->post('gruppe_kurzbz'),
+ $this->input->post('content_id')
+ ]);
+ $this->getDataOrTerminateWithError($result);
+
+ $this->terminateWithSuccess(true);
+ }
+
+ public function postChild()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
+ $this->form_validation->set_rules('child_content_id', 'Child Content ID', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $content_id = $this->input->post('content_id');
+
+ $sortResult = $this->ContentchildModel->getMaxSort($content_id);
+ $sort = $this->getDataOrTerminateWithError($sortResult) + 1;
+
+ // LEGACY-QUIRK: tbl_contentchild has no unique index on (content_id,
+ // child_content_id). The legacy code does not check for a duplicate, so a child can be
+ // attached more than once. Kept as a functional copy. See section 3 of the contract.
+ $result = $this->ContentchildModel->insert([
+ 'content_id' => $content_id,
+ 'child_content_id' => $this->input->post('child_content_id'),
+ 'sort' => $sort,
+ 'insertamum' => date('Y-m-d H:i:s'),
+ 'insertvon' => getAuthUID()
+ ]);
+ $this->getDataOrTerminateWithError($result);
+
+ $this->terminateWithSuccess(true);
+ }
+
+ public function deleteChild()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('contentchild_id', 'Contentchild ID', 'required|is_natural');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $result = $this->ContentchildModel->delete($this->input->post('contentchild_id'));
+ $this->getDataOrTerminateWithError($result);
+
+ $this->terminateWithSuccess(true);
+ }
+
+ public function putChildSort()
+ {
+ if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
+ $this->terminateWithError('cms/keineBerechtigung');
+
+ $this->load->library('form_validation');
+ $this->form_validation->set_data($_POST);
+ $this->form_validation->set_rules('contentchild_id', 'Contentchild ID', 'required|is_natural');
+ $this->form_validation->set_rules('direction', 'Direction', 'required|in_list[up,down]');
+ if ($this->form_validation->run() == FALSE)
+ $this->terminateWithValidationErrors($this->form_validation->error_array());
+
+ $result = $this->ContentchildModel->swapSort(
+ $this->input->post('contentchild_id'),
+ $this->input->post('direction')
+ );
+ $this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
+ }
+}
diff --git a/application/libraries/CmsAdminLib.php b/application/libraries/CmsAdminLib.php
new file mode 100644
index 000000000..dd729b43f
--- /dev/null
+++ b/application/libraries/CmsAdminLib.php
@@ -0,0 +1,557 @@
+ci =& get_instance();
+
+ $this->ci->load->model('content/Content_model', 'ContentModel');
+ $this->ci->load->model('content/Contentsprache_model', 'ContentspracheModel');
+ $this->ci->load->model('content/Contentchild_model', 'ContentchildModel');
+ $this->ci->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
+ $this->ci->load->model('content/Contentlog_model', 'ContentlogModel');
+ $this->ci->load->model('content/Template_model', 'TemplateModel');
+
+ $this->ci->load->library('XsdSchemaLib');
+ $this->ci->load->library('PermissionLib');
+ $this->ci->config->load('cms');
+ }
+
+ /**
+ * @return object success(array) entitled oe_kurzbz values
+ */
+ public function getEntitledOe()
+ {
+ $oe = $this->ci->permissionlib->getOE_isEntitledFor('basis/cms');
+
+ if ($oe === false || !is_array($oe))
+ return success([]);
+
+ return success($oe);
+ }
+
+ // LEGACY-QUIRK: the legacy code checks the organisational unit only when it loads the
+ // page. It does not check on each write. This method exists, but it is called only
+ // where the legacy code calls it. See Q7 in the contract.
+ /**
+ * @param int $content_id
+ * @return object success(bool)
+ */
+ public function isEntitledForContent($content_id)
+ {
+ $oeResult = $this->ci->ContentModel->getOeKurzbz($content_id);
+ if (isError($oeResult))
+ return $oeResult;
+
+ $entitledResult = $this->getEntitledOe();
+ if (isError($entitledResult))
+ return $entitledResult;
+
+ return success(in_array(getData($oeResult), getData($entitledResult)));
+ }
+
+ /**
+ * @return object success(string) or error
+ */
+ public function getDefaultOe()
+ {
+ $default = $this->ci->config->item('default_oe_kurzbz');
+
+ $entitledResult = $this->getEntitledOe();
+ if (isError($entitledResult))
+ return $entitledResult;
+ $entitled = getData($entitledResult);
+
+ if (empty($entitled))
+ return error('cms/keineBerechtigteOe');
+
+ if (in_array($default, $entitled))
+ return success($default);
+
+ return success($entitled[0]);
+ }
+
+ /**
+ * @param int|null $parent_content_id
+ * @return object success(array) with content_id, sprache, version
+ */
+ public function createContent($parent_content_id = null)
+ {
+ $templatesResult = $this->ci->TemplateModel->loadWhere([]);
+ if (isError($templatesResult))
+ return $templatesResult;
+ $templates = getData($templatesResult);
+ if (empty($templates))
+ return error('cms/keineVorlageVorhanden');
+ $template = $templates[0];
+
+ $oeResult = $this->getDefaultOe();
+ if (isError($oeResult))
+ return $oeResult;
+ $oe = getData($oeResult);
+
+ $this->ci->load->library('PhrasesLib');
+ $phraseResult = $this->ci->phraseslib->getPhrases(
+ 'cms', DEFAULT_LANGUAGE, 'neuerEintrag', null, null, 'no'
+ );
+ $titel = $phraseResult->retval[0]->text;
+
+ $now = date('Y-m-d H:i:s');
+ $uid = getAuthUID();
+
+ $this->ci->db->trans_start();
+
+ $contentResult = $this->ci->ContentModel->insert([
+ 'template_kurzbz' => $template->template_kurzbz,
+ 'oe_kurzbz' => $oe,
+ 'aktiv' => true,
+ 'menu_open' => false,
+ 'beschreibung' => '',
+ 'insertamum' => $now,
+ 'insertvon' => $uid
+ ]);
+ if (isError($contentResult))
+ return $contentResult;
+ $content_id = getData($contentResult);
+
+ $spracheResult = $this->ci->ContentspracheModel->insert([
+ 'content_id' => $content_id,
+ 'sprache' => DEFAULT_LANGUAGE,
+ 'version' => 1,
+ 'sichtbar' => true,
+ 'titel' => $titel,
+ 'content' => ' ',
+ 'insertamum' => $now,
+ 'insertvon' => $uid
+ ]);
+ if (isError($spracheResult))
+ return $spracheResult;
+
+ if ($parent_content_id !== null)
+ {
+ $maxSortResult = $this->ci->ContentchildModel->getMaxSort($parent_content_id);
+ if (isError($maxSortResult))
+ return $maxSortResult;
+
+ $childResult = $this->ci->ContentchildModel->insert([
+ 'content_id' => $parent_content_id,
+ 'child_content_id' => $content_id,
+ 'sort' => getData($maxSortResult) + 1,
+ 'insertamum' => $now,
+ 'insertvon' => $uid
+ ]);
+ if (isError($childResult))
+ return $childResult;
+ }
+
+ $this->ci->db->trans_complete();
+
+ if ($this->ci->db->trans_status() === false)
+ return error('cms/speichernFehlgeschlagen');
+
+ return success([
+ 'content_id' => $content_id,
+ 'sprache' => DEFAULT_LANGUAGE,
+ 'version' => 1
+ ]);
+ }
+
+ /**
+ * @param int $content_id
+ * @param string $sprache source language
+ * @param int $version source version
+ * @param string $neueSprache target language
+ * @return object success(array) with contentsprache_id
+ */
+ public function createTranslation($content_id, $sprache, $version, $neueSprache)
+ {
+ $existsResult = $this->ci->ContentspracheModel->exists($content_id, $neueSprache);
+ if (isError($existsResult))
+ return $existsResult;
+ if (getData($existsResult))
+ return error('cms/uebersetzungExistiert');
+
+ $sourceResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
+ if (isError($sourceResult))
+ return $sourceResult;
+ $source = getData($sourceResult);
+
+ $now = date('Y-m-d H:i:s');
+ $uid = getAuthUID();
+
+ $insertResult = $this->ci->ContentspracheModel->insert([
+ 'content_id' => $content_id,
+ 'sprache' => $neueSprache,
+ 'version' => $source->version,
+ 'sichtbar' => true,
+ 'titel' => $source->titel,
+ 'content' => $source->content,
+ // The review feature is gone from this admin, but the columns stay and the
+ // legacy admin still writes them. A copied row must not inherit the review
+ // stamp of its source, so we clear it here.
+ 'reviewvon' => null,
+ 'reviewamum' => null,
+ 'gesperrt_uid' => null,
+ 'insertamum' => $now,
+ 'insertvon' => $uid,
+ 'updateamum' => $now,
+ 'updatevon' => $uid
+ ]);
+ if (isError($insertResult))
+ return $insertResult;
+
+ return success(['contentsprache_id' => getData($insertResult)]);
+ }
+
+ /**
+ * @param int $content_id
+ * @param string $sprache
+ * @return object success(array) with version
+ */
+ public function createVersion($content_id, $sprache)
+ {
+ $sourceResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, null);
+ if (isError($sourceResult))
+ return $sourceResult;
+ $source = getData($sourceResult);
+
+ $maxResult = $this->ci->ContentspracheModel->getMaxVersion($content_id, $sprache);
+ if (isError($maxResult))
+ return $maxResult;
+ $newVersion = getData($maxResult) + 1;
+
+ $now = date('Y-m-d H:i:s');
+ $uid = getAuthUID();
+
+ $insertResult = $this->ci->ContentspracheModel->insert([
+ 'content_id' => $content_id,
+ 'sprache' => $sprache,
+ 'version' => $newVersion,
+ 'sichtbar' => false,
+ 'titel' => $source->titel,
+ 'content' => $source->content,
+ // The review feature is gone from this admin, but the columns stay and the
+ // legacy admin still writes them. A copied row must not inherit the review
+ // stamp of its source, so we clear it here.
+ 'reviewvon' => null,
+ 'reviewamum' => null,
+ 'gesperrt_uid' => null,
+ 'insertamum' => $now,
+ 'insertvon' => $uid,
+ 'updateamum' => $now,
+ 'updatevon' => $uid
+ ]);
+ if (isError($insertResult))
+ return $insertResult;
+
+ return success(['version' => $newVersion]);
+ }
+
+ /**
+ * @param array $daten fields from contract 7.7
+ * @return object success(true) or error
+ */
+ public function saveProperties($daten)
+ {
+ $versionResult = $this->ci->ContentspracheModel->getOne(
+ $daten['content_id'], $daten['sprache'], $daten['version']
+ );
+ if (isError($versionResult))
+ return $versionResult;
+ $row = getData($versionResult);
+
+ $now = date('Y-m-d H:i:s');
+ $uid = getAuthUID();
+
+ $this->ci->db->trans_start();
+
+ $this->ci->ContentModel->update($daten['content_id'], [
+ 'template_kurzbz' => $daten['template_kurzbz'],
+ 'oe_kurzbz' => $daten['oe_kurzbz'],
+ 'aktiv' => $daten['aktiv'],
+ 'menu_open' => $daten['menu_open'],
+ 'beschreibung' => $daten['beschreibung'],
+ 'updateamum' => $now,
+ 'updatevon' => $uid
+ ]);
+
+ // LEGACY-QUIRK: prefs_save sets updateamum and updatevon in tbl_content only.
+ // It does not touch tbl_contentsprache. Kept as a functional copy.
+ $this->ci->ContentspracheModel->update($row->contentsprache_id, [
+ 'titel' => $daten['titel'],
+ 'sichtbar' => $daten['sichtbar']
+ ]);
+
+ $this->ci->db->trans_complete();
+
+ if ($this->ci->db->trans_status() === false)
+ return error('cms/speichernFehlgeschlagen');
+
+ return success(true);
+ }
+
+ // LEGACY-QUIRK: the legacy XSDFormPrinter_XML branch checks neither the permission
+ // type nor the lock. The form appears only when the user holds the lock, but the POST
+ // itself is unprotected. See Q3 in the contract.
+ /**
+ * @param int $content_id
+ * @param string $sprache
+ * @param int $version
+ * @param array $values field name => value
+ * @return object success(true) or error
+ */
+ public function saveContentXml($content_id, $sprache, $version, $values)
+ {
+ $versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
+ if (isError($versionResult))
+ return $versionResult;
+ $row = getData($versionResult);
+
+ $contentResult = $this->ci->ContentModel->load($content_id);
+ if (isError($contentResult))
+ return $contentResult;
+ $content = getData($contentResult);
+ if (empty($content))
+ return error('cms/contentNichtGefunden');
+
+ $templateResult = $this->ci->TemplateModel->load($content[0]->template_kurzbz);
+ if (isError($templateResult))
+ return $templateResult;
+ $template = getData($templateResult);
+ if (empty($template))
+ return error('cms/keineVorlageVorhanden');
+
+ $schemaResult = $this->ci->xsdschemalib->parseSchema(
+ $template[0]->xsd, $content[0]->template_kurzbz
+ );
+ if (isError($schemaResult))
+ return $schemaResult;
+
+ $xmlResult = $this->ci->xsdschemalib->buildXml(getData($schemaResult), $values);
+ if (isError($xmlResult))
+ return $xmlResult;
+
+ $updateResult = $this->ci->ContentspracheModel->update(
+ $row->contentsprache_id,
+ ['content' => getData($xmlResult)]
+ );
+ if (isError($updateResult))
+ return $updateResult;
+
+ return success(true);
+ }
+
+ // LEGACY-QUIRK: content::sperren does not check for an existing lock.
+ // A second user overwrites the first lock. See Q2 in the contract.
+ /**
+ * @param int $contentsprache_id
+ * @return object success(true) or error
+ */
+ public function lock($contentsprache_id)
+ {
+ $uid = getAuthUID();
+
+ $this->ci->db->trans_start();
+
+ $logResult = $this->ci->ContentlogModel->insert([
+ 'uid' => $uid,
+ 'contentsprache_id' => $contentsprache_id,
+ 'start' => date('Y-m-d H:i:s')
+ ]);
+ if (isError($logResult))
+ return $logResult;
+
+ $updateResult = $this->ci->ContentspracheModel->update($contentsprache_id, [
+ 'gesperrt_uid' => $uid
+ ]);
+ if (isError($updateResult))
+ return $updateResult;
+
+ $this->ci->db->trans_complete();
+
+ if ($this->ci->db->trans_status() === false)
+ return error('cms/speichernFehlgeschlagen');
+
+ return success(true);
+ }
+
+ // LEGACY-QUIRK: content::freigabeUser releases ALL locks of the user, not only the
+ // lock on this page. Kept as a functional copy. See Q1 in the contract.
+ /**
+ * @return object success(true)
+ */
+ public function releaseOwnLocks()
+ {
+ $uid = getAuthUID();
+
+ $this->ci->db->trans_start();
+
+ $this->ci->db->query(
+ 'UPDATE campus.tbl_contentlog SET ende = now() WHERE uid = ? AND ende IS NULL',
+ [$uid]
+ );
+
+ $this->ci->db->query(
+ 'UPDATE campus.tbl_contentsprache SET gesperrt_uid = NULL WHERE gesperrt_uid = ?',
+ [$uid]
+ );
+
+ $this->ci->db->trans_complete();
+
+ if ($this->ci->db->trans_status() === false)
+ return error('cms/speichernFehlgeschlagen');
+
+ return success(true);
+ }
+
+ /**
+ * @param int $contentsprache_id
+ * @return object success(true)
+ */
+ public function forceRelease($contentsprache_id)
+ {
+ $this->ci->db->trans_start();
+
+ $this->ci->db->query(
+ 'UPDATE campus.tbl_contentlog SET ende = now() WHERE contentsprache_id = ? AND ende IS NULL',
+ [$contentsprache_id]
+ );
+
+ $this->ci->db->query(
+ 'UPDATE campus.tbl_contentsprache SET gesperrt_uid = NULL WHERE contentsprache_id = ?',
+ [$contentsprache_id]
+ );
+
+ $this->ci->db->trans_complete();
+
+ if ($this->ci->db->trans_status() === false)
+ return error('cms/speichernFehlgeschlagen');
+
+ return success(true);
+ }
+
+ /**
+ * @param int $content_id
+ * @param string $sprache
+ * @param int $version
+ * @return object success(array) lock state per contract 7.8
+ */
+ public function getLockState($content_id, $sprache, $version)
+ {
+ $versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
+ if (isError($versionResult))
+ return $versionResult;
+ $row = getData($versionResult);
+
+ $lockResult = $this->ci->ContentlogModel->getOpenLock($row->contentsprache_id);
+ $logEntry = (!isError($lockResult)) ? getData($lockResult) : null;
+
+ // LEGACY-QUIRK: admin.php inserts an empty string, releasing writes NULL. Both mean
+ // free. The contract reports NULL.
+ $gesperrt_uid = ($row->gesperrt_uid === '') ? null : $row->gesperrt_uid;
+
+ return success([
+ 'contentsprache_id' => (int) $row->contentsprache_id,
+ 'gesperrt_uid' => $gesperrt_uid,
+ 'start' => $logEntry ? $logEntry->start : null,
+ 'own' => ($gesperrt_uid !== null && $gesperrt_uid === getAuthUID()),
+ 'may_force' => $this->ci->permissionlib->isBerechtigt('basis/cms_sperrfreigabe', 'su')
+ ]);
+ }
+
+ // DEVIATION: admin.php ignores the return value of deleteContent and tells the user
+ // nothing. This code reports the error. See Q5 in the contract.
+ /**
+ * @param int $content_id
+ * @return object success(true) or error
+ */
+ public function deleteContent($content_id)
+ {
+ $this->ci->db->trans_start();
+
+ $this->ci->db->query(
+ 'DELETE FROM campus.tbl_contentchild WHERE content_id = ?',
+ [$content_id]
+ );
+
+ $this->ci->db->query(
+ 'DELETE FROM campus.tbl_contentchild WHERE child_content_id = ?',
+ [$content_id]
+ );
+
+ $this->ci->db->query(
+ 'DELETE FROM campus.tbl_contentlog WHERE contentsprache_id IN (SELECT contentsprache_id FROM campus.tbl_contentsprache WHERE content_id = ?)',
+ [$content_id]
+ );
+
+ $this->ci->db->query(
+ 'DELETE FROM campus.tbl_contentsprache WHERE content_id = ?',
+ [$content_id]
+ );
+
+ $this->ci->db->query(
+ 'DELETE FROM campus.tbl_contentgruppe WHERE content_id = ?',
+ [$content_id]
+ );
+
+ $this->ci->db->query(
+ 'DELETE FROM campus.tbl_content WHERE content_id = ?',
+ [$content_id]
+ );
+
+ $this->ci->db->trans_complete();
+
+ if ($this->ci->db->trans_status() === false)
+ return error('cms/loeschenNichtMoeglichReferenzen');
+
+ return success(true);
+ }
+
+ /**
+ * @param int $content_id
+ * @param string $sprache
+ * @param int $version
+ * @return object success(true) or error
+ */
+ public function deleteVersion($content_id, $sprache, $version)
+ {
+ if ($sprache === DEFAULT_LANGUAGE)
+ {
+ $countResult = $this->ci->ContentspracheModel->getNumberOfVersions($content_id, $sprache);
+ if (isError($countResult))
+ return $countResult;
+ if (getData($countResult) === 1)
+ return error('cms/letzteVersionNichtLoeschbar');
+ }
+
+ $versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
+ if (isError($versionResult))
+ return $versionResult;
+ $row = getData($versionResult);
+
+ $this->ci->db->trans_start();
+
+ $this->ci->db->query(
+ 'DELETE FROM campus.tbl_contentlog WHERE contentsprache_id = ?',
+ [$row->contentsprache_id]
+ );
+
+ $this->ci->db->query(
+ 'DELETE FROM campus.tbl_contentsprache WHERE content_id = ? AND sprache = ? AND version = ?',
+ [$content_id, $sprache, $version]
+ );
+
+ $this->ci->db->trans_complete();
+
+ if ($this->ci->db->trans_status() === false)
+ return error('cms/loeschenFehlgeschlagen');
+
+ return success(true);
+ }
+}
diff --git a/application/libraries/XsdSchemaLib.php b/application/libraries/XsdSchemaLib.php
new file mode 100644
index 000000000..ea38c2a55
--- /dev/null
+++ b/application/libraries/XsdSchemaLib.php
@@ -0,0 +1,188 @@
+loadXML($xsd))
+ {
+ libxml_clear_errors();
+ return error('cms/xsdUngueltig');
+ }
+
+ libxml_clear_errors();
+
+ $root = null;
+ $fields = array();
+ $this->_walkElements($dom->documentElement, $root, $fields);
+
+ return success(array(
+ 'template_kurzbz' => $templateKurzbz,
+ 'root' => $root,
+ 'fields' => $fields
+ ));
+ }
+
+ /**
+ * Reads the values of a content from the XML.
+ * @param string $xml the value of tbl_contentsprache.content
+ * @param array $schema the result of parseSchema
+ * @return object success(array) field name => value
+ */
+ public function extractValues($xml, $schema)
+ {
+ $values = array();
+
+ foreach ($schema['fields'] as $field)
+ {
+ $values[$field['name']] = '';
+ }
+
+ if ($xml === null || trim($xml) === '')
+ {
+ return success($values);
+ }
+
+ libxml_use_internal_errors(true);
+ $dom = new DOMDocument();
+
+ if (!$dom->loadXML($xml))
+ {
+ libxml_clear_errors();
+ return success($values);
+ }
+
+ libxml_clear_errors();
+
+ foreach ($schema['fields'] as $field)
+ {
+ $nodes = $dom->getElementsByTagName($field['name']);
+ if ($nodes->length > 0)
+ {
+ $values[$field['name']] = $nodes->item(0)->nodeValue;
+ }
+ }
+
+ return success($values);
+ }
+
+ /**
+ * Builds the XML from the values.
+ * @param array $schema the result of parseSchema
+ * @param array $values field name => value
+ * @return object success(string) the XML
+ */
+ public function buildXml($schema, $values)
+ {
+ $xml = '';
+ $xml .= "\n<" . $schema['root'] . '>';
+
+ foreach ($schema['fields'] as $field)
+ {
+ $value = isset($values[$field['name']]) ? $values[$field['name']] : '';
+ $xml .= "\n<" . $field['name'] . '>_escapeCdata($value) . ']]>' . $field['name'] . '>';
+ }
+
+ $xml .= "\n" . $schema['root'] . '>';
+
+ libxml_use_internal_errors(true);
+ $check = new DOMDocument();
+
+ if (!$check->loadXML($xml))
+ {
+ libxml_clear_errors();
+ return error('cms/xmlUngueltig');
+ }
+
+ libxml_clear_errors();
+
+ return success($xml);
+ }
+
+ // -------------------------------------------------------------------------
+ // Private methods
+
+ /**
+ * Walks xs:element nodes recursively.
+ * The first element without a type attribute is the root.
+ * Elements with a type attribute are fields.
+ * xs:complexType and xs:sequence are traversed and ignored.
+ */
+ private function _walkElements($node, &$root, &$fields)
+ {
+ if ($node->nodeType !== XML_ELEMENT_NODE)
+ {
+ return;
+ }
+
+ $localName = $node->localName;
+
+ if ($localName === 'element')
+ {
+ $typeAttr = $node->getAttribute('type');
+
+ if ($typeAttr === '')
+ {
+ if ($root === null)
+ {
+ $root = $node->getAttribute('name');
+ }
+ }
+ else
+ {
+ $minOccurs = $node->getAttribute('minOccurs');
+ $required = ($minOccurs === '') ? true : ((int)$minOccurs > 0);
+
+ $fields[] = array(
+ 'name' => $node->getAttribute('name'),
+ 'type' => $this->_mapType($typeAttr),
+ 'required' => $required
+ );
+ }
+ }
+
+ foreach ($node->childNodes as $child)
+ {
+ $this->_walkElements($child, $root, $fields);
+ }
+ }
+
+ /**
+ * Maps an XSD type to the field schema type.
+ */
+ private function _mapType($xsdType)
+ {
+ $map = array(
+ 'xs:string' => 'string',
+ 'xs:decimal' => 'string',
+ 'xs:integer' => 'string',
+ 'xs:positiveInteger' => 'string',
+ 'xs:date' => 'date',
+ 'wysiwyg' => 'wysiwyg',
+ 'file' => 'file',
+ 'boolean' => 'boolean'
+ );
+
+ return isset($map[$xsdType]) ? $map[$xsdType] : 'string';
+ }
+
+ // Escapes ]]> in the value. The legacy code does not do this and writes broken XML.
+ private function _escapeCdata($value)
+ {
+ return str_replace(']]>', ']]]]>', $value);
+ }
+}
diff --git a/application/models/content/Content_model.php b/application/models/content/Content_model.php
index 48a155cb2..0e4f082be 100644
--- a/application/models/content/Content_model.php
+++ b/application/models/content/Content_model.php
@@ -323,4 +323,209 @@ class Content_model extends DB_Model
return success(isset($result[$root_content_id]) ? $result[$root_content_id] : null);
}
+
+ /**
+ * Root contents without a parent in tbl_contentchild. Excludes news.
+ * @return stdClass success with array of rows or error
+ */
+ public function getRootContent()
+ {
+ $query = '
+ SELECT *
+ FROM (
+ SELECT DISTINCT ON (content_id) *
+ FROM campus.tbl_content
+ LEFT JOIN campus.tbl_contentchild USING (content_id)
+ WHERE tbl_content.template_kurzbz <> ?
+ AND content_id NOT IN (
+ SELECT child_content_id FROM campus.tbl_contentchild
+ WHERE child_content_id = tbl_content.content_id)
+ ) AS a
+ ORDER BY sort, content_id
+ ';
+
+ return $this->execReadOnlyQuery($query, ['news']);
+ }
+
+ /**
+ * Recent news from the last two months, max 100 rows.
+ * @return stdClass success with array of rows or error
+ */
+ public function getNewsContent()
+ {
+ $query = "
+ SELECT *
+ FROM campus.tbl_content
+ JOIN campus.tbl_news USING (content_id)
+ WHERE tbl_news.datum >= now() - '2 month'::interval
+ ORDER BY datum DESC
+ LIMIT 100
+ ";
+
+ return $this->execReadOnlyQuery($query);
+ }
+
+ /**
+ * Search contents by content_id or titel. Excludes news. Returns content_ids only.
+ * @param array $searchItems search terms
+ * @return stdClass success with array of rows or error
+ */
+ public function searchCms($searchItems)
+ {
+ if (empty($searchItems))
+ return success([]);
+
+ $conditions = [];
+ $params = [];
+ foreach ($searchItems as $term)
+ {
+ $conditions[] = '(content_id::text = ? OR lower(titel) LIKE lower(?))';
+ $params[] = $term;
+ $params[] = '%' . $term . '%';
+ }
+
+ $query = '
+ SELECT DISTINCT tbl_content.content_id
+ FROM campus.tbl_contentsprache
+ JOIN campus.tbl_content USING (content_id)
+ WHERE tbl_content.template_kurzbz <> ?
+ AND (' . implode(' OR ', $conditions) . ')
+ ORDER BY content_id
+ ';
+
+ array_unshift($params, 'news');
+
+ return $this->execReadOnlyQuery($query, $params);
+ }
+
+ /**
+ * All contents eligible as children: excludes ancestors, self, and news.
+ * @param int $content_id the content to find children for
+ * @param string $sprache language for the titel subselect
+ * @return stdClass success with array of rows or error
+ */
+ public function getPossibleChilds($content_id, $sprache)
+ {
+ $query = '
+ SELECT content_id, oe_kurzbz, template_kurzbz,
+ (SELECT titel FROM campus.tbl_contentsprache
+ WHERE sprache = ? AND content_id = tbl_content.content_id
+ ORDER BY version LIMIT 1) AS titel
+ FROM campus.tbl_content
+ WHERE content_id NOT IN (
+ WITH RECURSIVE parents(content_id, child_content_id) AS (
+ SELECT content_id, child_content_id FROM campus.tbl_contentchild
+ WHERE child_content_id = ?
+ UNION ALL
+ SELECT cc.content_id, cc.child_content_id
+ FROM campus.tbl_contentchild cc, parents
+ WHERE cc.child_content_id = parents.content_id)
+ SELECT content_id FROM parents GROUP BY content_id)
+ AND content_id <> ?
+ AND template_kurzbz <> ?
+ ORDER BY titel
+ ';
+
+ return $this->execReadOnlyQuery($query, [$sprache, $content_id, $content_id, 'news']);
+ }
+
+ /**
+ * Where a content is referenced. Returns [{table, label}] for the delete dialog.
+ * Tolerates missing addon/testtool schemas.
+ * @param int $content_id
+ * @return stdClass success with flat array or error
+ */
+ public function getUsage($content_id)
+ {
+ $coreQuery = "
+ SELECT 'campus.tbl_infoscreen_content' AS \"table\",
+ infoscreen_id::text AS label
+ FROM campus.tbl_infoscreen_content WHERE content_id = ?
+ UNION ALL
+ SELECT 'campus.tbl_news', betreff
+ FROM campus.tbl_news WHERE content_id = ?
+ UNION ALL
+ SELECT 'public.tbl_ort', ort_kurzbz
+ FROM public.tbl_ort WHERE content_id = ?
+ UNION ALL
+ SELECT 'public.tbl_service', bezeichnung
+ FROM public.tbl_service WHERE content_id = ?
+ UNION ALL
+ SELECT 'public.tbl_statistik', bezeichnung
+ FROM public.tbl_statistik WHERE content_id = ?
+ ";
+ $coreParams = [$content_id, $content_id, $content_id, $content_id, $content_id];
+
+ $coreResult = $this->execReadOnlyQuery($coreQuery, $coreParams);
+ $rows = [];
+
+ if (!isError($coreResult) && getData($coreResult))
+ $rows = getData($coreResult);
+
+ // Optional schemas: testtool and addon may not exist. A failing query still writes a
+ // db error into the response envelope, so check for the table before querying it.
+ $optionalQueries = [
+ [
+ 'testtool', 'tbl_ablauf_vorgaben',
+ "SELECT 'testtool.tbl_ablauf_vorgaben' AS \"table\",
+ ablauf_vorgaben_id::text AS label
+ FROM testtool.tbl_ablauf_vorgaben WHERE content_id = ?"
+ ],
+ [
+ 'addon', 'tbl_software',
+ "SELECT 'addon.tbl_software' AS \"table\",
+ software_id::text AS label
+ FROM addon.tbl_software WHERE content_id = ?"
+ ]
+ ];
+
+ foreach ($optionalQueries as $opt)
+ {
+ if (!$this->tableExists($opt[0], $opt[1]))
+ continue;
+
+ $result = $this->execReadOnlyQuery($opt[2], [$content_id]);
+ if (!isError($result) && getData($result))
+ $rows = array_merge($rows, getData($result));
+ }
+
+ return success($rows);
+ }
+
+ /**
+ * True if a base table exists. Guards the optional testtool and addon schemas.
+ * @param string $schema
+ * @param string $table
+ * @return bool
+ */
+ private function tableExists($schema, $table)
+ {
+ $query = "
+ SELECT 1 AS exists
+ FROM information_schema.tables
+ WHERE table_catalog = ? AND table_schema = ? AND table_name = ?
+ ";
+
+ return hasData($this->execReadOnlyQuery($query, [DB_NAME, $schema, $table]));
+ }
+
+ /**
+ * Returns oe_kurzbz for a content as a string.
+ * @param int $content_id
+ * @return stdClass success with string or error
+ */
+ public function getOeKurzbz($content_id)
+ {
+ $query = 'SELECT oe_kurzbz FROM campus.tbl_content WHERE content_id = ?';
+ $result = $this->execReadOnlyQuery($query, [$content_id]);
+
+ if (isError($result))
+ return $result;
+
+ $data = getData($result);
+ if (empty($data))
+ return error('Content not found');
+
+ return success($data[0]->oe_kurzbz);
+ }
}
diff --git a/application/models/content/Contentchild_model.php b/application/models/content/Contentchild_model.php
index 11738d144..fb468db0e 100644
--- a/application/models/content/Contentchild_model.php
+++ b/application/models/content/Contentchild_model.php
@@ -11,4 +11,140 @@ class Contentchild_model extends DB_Model
$this->dbTable = 'campus.tbl_contentchild';
$this->pk = 'contentchild_id';
}
+
+ /**
+ * Children with titel, sorted by sort.
+ * @param int $content_id
+ * @param string $sprache
+ * @return stdClass success with array of rows or error
+ */
+ public function getChilds($content_id, $sprache)
+ {
+ $query = '
+ SELECT cc.contentchild_id, cc.child_content_id, cc.sort,
+ (SELECT titel FROM campus.tbl_contentsprache
+ WHERE content_id = cc.child_content_id AND sprache = ?
+ ORDER BY version LIMIT 1) AS titel
+ FROM campus.tbl_contentchild cc
+ WHERE cc.content_id = ?
+ ORDER BY cc.sort, cc.contentchild_id
+ ';
+
+ return $this->execReadOnlyQuery($query, [$sprache, $content_id]);
+ }
+
+ /**
+ * Highest sort value. Returns 0 if no row exists.
+ * @param int $content_id
+ * @return stdClass success with int or error
+ */
+ public function getMaxSort($content_id)
+ {
+ $query = '
+ SELECT COALESCE(MAX(sort), 0) AS max_sort
+ FROM campus.tbl_contentchild
+ WHERE content_id = ?
+ ';
+
+ $result = $this->execReadOnlyQuery($query, [$content_id]);
+ if (isError($result))
+ return $result;
+
+ return success((int) getData($result)[0]->max_sort);
+ }
+
+ /**
+ * All descendant content_ids recursively. Returns a flat array.
+ * @param int $content_id
+ * @return stdClass success with array of ints or error
+ */
+ public function getAllChildIds($content_id)
+ {
+ $query = '
+ WITH RECURSIVE childs(content_id, child_content_id) AS (
+ SELECT content_id, child_content_id FROM campus.tbl_contentchild WHERE content_id = ?
+ UNION ALL
+ SELECT cc.content_id, cc.child_content_id
+ FROM campus.tbl_contentchild cc, childs
+ WHERE cc.content_id = childs.child_content_id)
+ SELECT DISTINCT child_content_id FROM childs
+ ';
+
+ $result = $this->execReadOnlyQuery($query, [$content_id]);
+ if (isError($result))
+ return $result;
+
+ $ids = [];
+ if (getData($result))
+ {
+ foreach (getData($result) as $row)
+ $ids[] = (int) $row->child_content_id;
+ }
+
+ return success($ids);
+ }
+
+ /**
+ * Swap sort value with the neighbour. Direction is 'up' or 'down'.
+ * @param int $contentchild_id
+ * @param string $direction 'up' or 'down'
+ * @return stdClass success or error
+ */
+ public function swapSort($contentchild_id, $direction)
+ {
+ $currentResult = $this->load($contentchild_id);
+ if (isError($currentResult))
+ return $currentResult;
+ $current = getData($currentResult)[0];
+
+ if ($direction === 'up')
+ {
+ $neighbourQuery = '
+ SELECT contentchild_id, sort
+ FROM campus.tbl_contentchild
+ WHERE content_id = ? AND sort < ?
+ ORDER BY sort DESC
+ LIMIT 1
+ ';
+ }
+ else
+ {
+ $neighbourQuery = '
+ SELECT contentchild_id, sort
+ FROM campus.tbl_contentchild
+ WHERE content_id = ? AND sort > ?
+ ORDER BY sort ASC
+ LIMIT 1
+ ';
+ }
+
+ $neighbourResult = $this->execReadOnlyQuery($neighbourQuery, [$current->content_id, $current->sort]);
+ if (isError($neighbourResult))
+ return $neighbourResult;
+
+ $neighbourData = getData($neighbourResult);
+ if (empty($neighbourData))
+ return error($direction === 'up' ? 'cms/bereitsGanzOben' : 'cms/bereitsGanzUnten');
+
+ $neighbour = $neighbourData[0];
+
+ // Transaction wraps the swap; the legacy does not.
+ $this->db->trans_start();
+
+ $this->execQuery(
+ 'UPDATE campus.tbl_contentchild SET sort = ? WHERE contentchild_id = ?',
+ [$neighbour->sort, $current->contentchild_id]
+ );
+ $this->execQuery(
+ 'UPDATE campus.tbl_contentchild SET sort = ? WHERE contentchild_id = ?',
+ [$current->sort, $neighbour->contentchild_id]
+ );
+
+ $this->db->trans_complete();
+
+ if ($this->db->trans_status() === false)
+ return error('Sort swap failed');
+
+ return success(true);
+ }
}
diff --git a/application/models/content/Contentgruppe_model.php b/application/models/content/Contentgruppe_model.php
index 23dc897a1..aadb9d7f8 100644
--- a/application/models/content/Contentgruppe_model.php
+++ b/application/models/content/Contentgruppe_model.php
@@ -56,5 +56,77 @@ class Contentgruppe_model extends DB_Model
return $result;
return success(!!getData($result));
}
-
+
+ /**
+ * Groups assigned to a content, with bezeichnung.
+ * @param int $content_id
+ * @return stdClass success with array of rows or error
+ */
+ public function getGruppen($content_id)
+ {
+ $query = '
+ SELECT cg.gruppe_kurzbz, g.bezeichnung
+ FROM campus.tbl_contentgruppe cg
+ JOIN public.tbl_gruppe g USING (gruppe_kurzbz)
+ WHERE cg.content_id = ?
+ ORDER BY cg.gruppe_kurzbz
+ ';
+
+ return $this->execReadOnlyQuery($query, [$content_id]);
+ }
+
+ /**
+ * All selectable groups. Returns gruppe_kurzbz and bezeichnung.
+ * @return stdClass success with array of rows or error
+ */
+ public function getAllGruppen()
+ {
+ // content_visible marks a group as selectable for content rights. Generated groups
+ // carry FALSE. admin.php filters the same way via getgruppe(...,$content_visible=true).
+ $query = '
+ SELECT gruppe_kurzbz, bezeichnung
+ FROM public.tbl_gruppe
+ WHERE content_visible = TRUE
+ ORDER BY gruppe_kurzbz
+ ';
+
+ return $this->execReadOnlyQuery($query);
+ }
+
+ /**
+ * Groups for multiple contents in one query. Returns content_id => [gruppe_kurzbz, ...].
+ * @param array $contentIds array of content_id values
+ * @return stdClass success with associative array or error
+ */
+ public function getGruppenForContents($contentIds)
+ {
+ if (empty($contentIds))
+ return success([]);
+
+ $placeholders = implode(',', array_fill(0, count($contentIds), '?'));
+ $query = '
+ SELECT content_id, gruppe_kurzbz
+ FROM campus.tbl_contentgruppe
+ WHERE content_id IN (' . $placeholders . ')
+ ORDER BY content_id, gruppe_kurzbz
+ ';
+
+ $result = $this->execReadOnlyQuery($query, array_values($contentIds));
+ if (isError($result))
+ return $result;
+
+ $map = [];
+ if (getData($result))
+ {
+ foreach (getData($result) as $row)
+ {
+ $id = (int) $row->content_id;
+ if (!isset($map[$id]))
+ $map[$id] = [];
+ $map[$id][] = $row->gruppe_kurzbz;
+ }
+ }
+
+ return success($map);
+ }
}
diff --git a/application/models/content/Contentlog_model.php b/application/models/content/Contentlog_model.php
index 025cb94aa..cd37eab68 100644
--- a/application/models/content/Contentlog_model.php
+++ b/application/models/content/Contentlog_model.php
@@ -11,4 +11,26 @@ class Contentlog_model extends DB_Model
$this->dbTable = 'campus.tbl_contentlog';
$this->pk = 'contentlog_id';
}
+
+ /**
+ * The open lock entry (ende IS NULL). Returns null if none exists.
+ * @param int $contentsprache_id
+ * @return stdClass success with row or null, or error
+ */
+ public function getOpenLock($contentsprache_id)
+ {
+ $query = '
+ SELECT contentlog_id, uid, contentsprache_id, start
+ FROM campus.tbl_contentlog
+ WHERE contentsprache_id = ? AND ende IS NULL
+ LIMIT 1
+ ';
+
+ $result = $this->execReadOnlyQuery($query, [$contentsprache_id]);
+ if (isError($result))
+ return $result;
+
+ $data = getData($result);
+ return success(empty($data) ? null : $data[0]);
+ }
}
diff --git a/application/models/content/Contentsprache_model.php b/application/models/content/Contentsprache_model.php
index 80b053e28..5f5ca65e5 100644
--- a/application/models/content/Contentsprache_model.php
+++ b/application/models/content/Contentsprache_model.php
@@ -38,5 +38,128 @@ class Contentsprache_model extends DB_Model
return success(!!getData($result));
}
-
+
+ /**
+ * All versions of a language, ascending by version.
+ * @param int $content_id
+ * @param string $sprache
+ * @return stdClass success with array of rows or error
+ */
+ public function getVersions($content_id, $sprache)
+ {
+ $query = '
+ SELECT contentsprache_id, version, sichtbar, titel,
+ insertamum, insertvon, updateamum, updatevon, gesperrt_uid
+ FROM campus.tbl_contentsprache
+ WHERE content_id = ? AND sprache = ?
+ ORDER BY version ASC
+ ';
+
+ return $this->execReadOnlyQuery($query, [$content_id, $sprache]);
+ }
+
+ /**
+ * Distinct languages for a content. Returns a flat array of strings.
+ * @param int $content_id
+ * @return stdClass success with array of strings or error
+ */
+ public function getLanguages($content_id)
+ {
+ $query = '
+ SELECT DISTINCT sprache
+ FROM campus.tbl_contentsprache
+ WHERE content_id = ?
+ ORDER BY sprache
+ ';
+
+ $result = $this->execReadOnlyQuery($query, [$content_id]);
+ if (isError($result))
+ return $result;
+
+ $languages = [];
+ if (getData($result))
+ {
+ foreach (getData($result) as $row)
+ $languages[] = $row->sprache;
+ }
+
+ return success($languages);
+ }
+
+ /**
+ * Highest version number. Returns 0 if no row exists.
+ * @param int $content_id
+ * @param string $sprache
+ * @return stdClass success with int or error
+ */
+ public function getMaxVersion($content_id, $sprache)
+ {
+ $query = '
+ SELECT COALESCE(MAX(version), 0) AS max_version
+ FROM campus.tbl_contentsprache
+ WHERE content_id = ? AND sprache = ?
+ ';
+
+ $result = $this->execReadOnlyQuery($query, [$content_id, $sprache]);
+ if (isError($result))
+ return $result;
+
+ return success((int) getData($result)[0]->max_version);
+ }
+
+ /**
+ * Row count for a content+language. Used for the delete guard.
+ * @param int $content_id
+ * @param string $sprache
+ * @return stdClass success with int or error
+ */
+ public function getNumberOfVersions($content_id, $sprache)
+ {
+ $query = '
+ SELECT COUNT(*) AS cnt
+ FROM campus.tbl_contentsprache
+ WHERE content_id = ? AND sprache = ?
+ ';
+
+ $result = $this->execReadOnlyQuery($query, [$content_id, $sprache]);
+ if (isError($result))
+ return $result;
+
+ return success((int) getData($result)[0]->cnt);
+ }
+
+ /**
+ * One version, all columns. If $version is empty, returns the highest version.
+ * @param int $content_id
+ * @param string $sprache
+ * @param int|null $version
+ * @return stdClass success with single row or error
+ */
+ public function getOne($content_id, $sprache, $version = null)
+ {
+ $params = [$content_id, $sprache];
+
+ $versionClause = '';
+ if (!empty($version))
+ {
+ $versionClause = ' AND version = ?';
+ $params[] = $version;
+ }
+
+ $query = '
+ SELECT * FROM campus.tbl_contentsprache
+ WHERE content_id = ? AND sprache = ?' . $versionClause . '
+ ORDER BY version DESC LIMIT 1
+ ';
+
+ $result = $this->execReadOnlyQuery($query, $params);
+ if (isError($result))
+ return $result;
+
+ $data = getData($result);
+ if (empty($data))
+ return error('Version not found');
+
+ return success($data[0]);
+ }
}
diff --git a/application/views/Cms.php b/application/views/Cms.php
new file mode 100644
index 000000000..2a6a0cf46
--- /dev/null
+++ b/application/views/Cms.php
@@ -0,0 +1,33 @@
+ 'CMS',
+ 'axios027' => true,
+ 'bootstrap5' => true,
+ 'fontawesome6' => true,
+ 'vue3' => true,
+ 'primevue3' => true,
+ 'tabulator6' => true,
+ 'tinymce5' => true,
+ 'phrases' => array('global', 'ui', 'cms'),
+ 'customCSSs' => [
+ 'public/css/components/verticalsplit.css',
+ 'public/css/Cms.css'
+ ],
+ 'customJSs' => [
+ 'vendor/npm-asset/primevue/tree/tree.min.js',
+ 'vendor/npm-asset/primevue/checkbox/checkbox.min.js',
+ 'vendor/npm-asset/primevue/textarea/textarea.min.js',
+ 'vendor/moment/luxonjs/luxon.min.js'
+ ],
+ 'customJSModules' => ['public/js/apps/Cms.js']
+ );
+ $this->load->view('templates/FHC-Header', $includesArray);
+?>
+
+
+
+load->view('templates/FHC-Footer', $includesArray); ?>
diff --git a/cms/tinymce_dms.php b/cms/tinymce_dms.php
index 9971eb808..56f0915f4 100644
--- a/cms/tinymce_dms.php
+++ b/cms/tinymce_dms.php
@@ -83,12 +83,86 @@ if (! $rechte->isberechtigt('basis/dms', null, 's', null))
return confirm('Möchten Sie das File wirklich löschen?');
}
+ /*
+ * ---------------------------------------------------------------------------------
+ * CIS4 BRIDGE -- ticket 78489, CMS admin on CI3 and Vue
+ * ---------------------------------------------------------------------------------
+ *
+ * WHY THIS CHANGE IS HERE AND NOT SOMEWHERE ELSE
+ *
+ * The new CMS admin (application/controllers/Cms.php, public/js/components/Cms/) uses
+ * TinyMCE 5. This file returns its selection through the TinyMCE 3 API, that is
+ * through the global object tinyMCEPopup:
+ *
+ * tinyMCEPopup.getWindowArg("window") -> the calling window
+ * tinyMCEPopup.getWindowArg("input") -> the target field id in that window
+ * tinyMCEPopup.close() -> close this window
+ *
+ * tinyMCEPopup does not exist in TinyMCE 5. It came with the plugin "inlinepopups",
+ * which TinyMCE 4 removed. TinyMCE 5 uses file_picker_callback instead and expects
+ * the caller to hand back the URL.
+ *
+ * The return code must run IN THIS window, because only this window knows which
+ * dms_id the editor clicked. A wrapper in the new admin cannot catch that: it sees
+ * the opened window, not the click inside it. A proxy controller does not help for
+ * the same reason. The selection happens here.
+ *
+ * WHAT WAS DELIBERATELY NOT CHANGED
+ *
+ * The ticket excludes the DMS input interface, because branches for it are open.
+ * This change respects that:
+ *
+ * - It only adds. The legacy branch below stays line for line and still runs
+ * unchanged as soon as tinyMCEPopup is present.
+ * - It touches exactly one JavaScript function. No PHP, no markup, no query,
+ * no view, no layout.
+ * - It changes none of the four callers of mySubmit.
+ *
+ * When these branches are merged, only this one function needs attention.
+ *
+ * WHY NO GET PARAMETER TELLS THE TWO PATHS APART
+ *
+ * A switch such as ?cis4=1 on open looks obvious, but it does not hold: the file
+ * browser navigates internally (search, paging, category change) and rebuilds its
+ * links from $_REQUEST. An extra parameter is lost on the way, and the bridge would
+ * be dead after the first search. The runtime check through tinyMCEPopup and
+ * window.opener survives every internal navigation and needs no change to the links.
+ *
+ * THE CONTRACT WITH THE NEW ADMIN
+ *
+ * message: { typ: 'fhc-dms-selection', url: 'dms.php?id=' }
+ * target: window.opener
+ * origin: window.location.origin (the receiver checks against it)
+ *
+ * The URL stays RELATIVE. This is mandatory, not carelessness:
+ * - The legacy view runs under /cms/ and resolves dms.php?id=N directly.
+ * - The CIS4 view replaces the string 'dms.php' in CmsLib::getContent() with
+ * APP_ROOT . 'cms/dms.php'.
+ * An absolute URL breaks both paths. Stored content must stay interchangeable
+ * between the two interfaces.
+ *
+ * Receiving side: public/js/components/Cms/Form/FieldWysiwyg.js
+ * ---------------------------------------------------------------------------------
+ */
var FileBrowserDialog=
{
init: function(){
},
mySubmit : function (id) {
var URL = "dms.php?id="+id;
+
+ // CIS4 path: see the comment block above this function.
+ if (typeof tinyMCEPopup === 'undefined' && window.opener && !window.opener.closed) {
+ window.opener.postMessage({ typ: 'fhc-dms-selection', url: URL }, window.location.origin);
+ window.close();
+ return;
+ }
+
+ if (typeof tinyMCEPopup === 'undefined') {
+ alert('The calling window is no longer available.');
+ return;
+ }
+
var win = tinyMCEPopup.getWindowArg("window");
// insert information now
diff --git a/public/css/Cms.css b/public/css/Cms.css
new file mode 100644
index 000000000..f1996f30e
--- /dev/null
+++ b/public/css/Cms.css
@@ -0,0 +1,20 @@
+.cms-admin {
+ min-height: 0;
+}
+
+.cms-admin-tree {
+ width: 300px;
+ min-width: 200px;
+ max-width: 400px;
+ overflow-y: auto;
+ border-right: 1px solid var(--fhc-border, #dee2e6);
+}
+
+.cms-admin-main {
+ min-width: 0;
+ overflow: hidden;
+}
+
+.cms-admin-tab-content {
+ min-height: 0;
+}
diff --git a/public/js/api/factory/cmsadmin.js b/public/js/api/factory/cmsadmin.js
new file mode 100644
index 000000000..36454719a
--- /dev/null
+++ b/public/js/api/factory/cmsadmin.js
@@ -0,0 +1,217 @@
+export default {
+
+ // --- CmsAdmin ---
+
+ getTree(menu, filter) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdmin/getTree',
+ params: { menu, ...(filter ? { filter } : {}) }
+ };
+ },
+ getContent(content_id) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdmin/getContent',
+ params: { content_id }
+ };
+ },
+ getContentsprache(content_id, sprache, version) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdmin/getContentsprache',
+ params: { content_id, sprache, version }
+ };
+ },
+ getTemplates() {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdmin/getTemplates'
+ };
+ },
+ getOrganisationseinheiten() {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdmin/getOrganisationseinheiten'
+ };
+ },
+ getSprachen() {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdmin/getSprachen'
+ };
+ },
+ getUsage(content_id) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdmin/getUsage',
+ params: { content_id }
+ };
+ },
+ postContent(parent_content_id) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdmin/postContent',
+ params: { parent_content_id }
+ };
+ },
+ postTranslation(content_id, sprache, version, target_sprache) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdmin/postTranslation',
+ params: { content_id, sprache, version, target_sprache }
+ };
+ },
+ postVersion(content_id, sprache) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdmin/postVersion',
+ params: { content_id, sprache }
+ };
+ },
+ putProperties(content_id, sprache, version, template_kurzbz, oe_kurzbz, aktiv, menu_open, beschreibung, titel, sichtbar) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdmin/putProperties',
+ params: { content_id, sprache, version, template_kurzbz, oe_kurzbz, aktiv, menu_open, beschreibung, titel, sichtbar }
+ };
+ },
+ deleteContent(content_id) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdmin/deleteContent',
+ params: { content_id }
+ };
+ },
+ deleteContentsprache(content_id, sprache, version) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdmin/deleteContentsprache',
+ params: { content_id, sprache, version }
+ };
+ },
+
+ // --- CmsAdminInhalt ---
+
+ getFormSchema(template_kurzbz) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdminInhalt/getFormSchema',
+ params: { template_kurzbz }
+ };
+ },
+ getFormData(content_id, sprache, version) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdminInhalt/getFormData',
+ params: { content_id, sprache, version }
+ };
+ },
+ getLock(content_id, sprache, version) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdminInhalt/getLock',
+ params: { content_id, sprache, version }
+ };
+ },
+ getVersions(content_id, sprache) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdminInhalt/getVersions',
+ params: { content_id, sprache }
+ };
+ },
+ putFormData(content_id, sprache, version, values) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminInhalt/putFormData',
+ params: { content_id, sprache, version, values }
+ };
+ },
+ postLock(contentsprache_id) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminInhalt/postLock',
+ params: { contentsprache_id }
+ };
+ },
+ deleteLock(contentsprache_id) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminInhalt/deleteLock',
+ params: { contentsprache_id }
+ };
+ },
+ deleteLockForced(contentsprache_id) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminInhalt/deleteLockForced',
+ params: { contentsprache_id }
+ };
+ },
+
+ // --- CmsAdminStruktur ---
+
+ getGruppen(content_id) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdminStruktur/getGruppen',
+ params: { content_id }
+ };
+ },
+ getAllGruppen() {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdminStruktur/getAllGruppen'
+ };
+ },
+ getChilds(content_id, sprache) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdminStruktur/getChilds',
+ params: { content_id, sprache }
+ };
+ },
+ getPossibleChilds(content_id, sprache) {
+ return {
+ method: 'get',
+ url: '/api/frontend/v1/CmsAdminStruktur/getPossibleChilds',
+ params: { content_id, sprache }
+ };
+ },
+ postGruppe(content_id, gruppe_kurzbz) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminStruktur/postGruppe',
+ params: { content_id, gruppe_kurzbz }
+ };
+ },
+ deleteGruppe(content_id, gruppe_kurzbz) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminStruktur/deleteGruppe',
+ params: { content_id, gruppe_kurzbz }
+ };
+ },
+ postChild(content_id, child_content_id) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminStruktur/postChild',
+ params: { content_id, child_content_id }
+ };
+ },
+ deleteChild(contentchild_id) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminStruktur/deleteChild',
+ params: { contentchild_id }
+ };
+ },
+ putChildSort(contentchild_id, direction) {
+ return {
+ method: 'post',
+ url: '/api/frontend/v1/CmsAdminStruktur/putChildSort',
+ params: { contentchild_id, direction }
+ };
+ }
+};
diff --git a/public/js/apps/Cms.js b/public/js/apps/Cms.js
new file mode 100644
index 000000000..52bce29b5
--- /dev/null
+++ b/public/js/apps/Cms.js
@@ -0,0 +1,42 @@
+import CmsAdmin from "../components/Cms/CmsAdmin.js";
+import PluginsPhrasen from "../plugins/Phrasen.js";
+
+const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '')
+ + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
+
+const router = VueRouter.createRouter({
+ history: VueRouter.createWebHistory('/' + ciPath),
+ routes: [
+ {
+ name: 'index',
+ path: '/cms',
+ component: CmsAdmin,
+ children: [
+ {
+ name: 'content',
+ path: 'content/:content_id/:sprache/:version/:tab',
+ component: CmsAdmin
+ }
+ ]
+ },
+ { path: '/:pathMatch(.*)*', redirect: { name: 'index' } }
+ ]
+});
+
+FhcApps.router.makeExtendable(router);
+
+const app = Vue.createApp({
+ name: 'CmsApp'
+});
+
+FhcApps.makeExtendable(app);
+
+app
+ .use(router)
+ .use(primevue.config.default, {
+ zIndex: {
+ overlay: 1100
+ }
+ })
+ .use(PluginsPhrasen)
+ .mount('#main');
diff --git a/public/js/components/Cms/CmsAdmin.js b/public/js/components/Cms/CmsAdmin.js
new file mode 100644
index 000000000..6eb85804b
--- /dev/null
+++ b/public/js/components/Cms/CmsAdmin.js
@@ -0,0 +1,213 @@
+import ApiCmsAdmin from '../../api/factory/cmsadmin.js';
+import ContentTree from './Tree/ContentTree.js';
+import ContentHeader from './Header/ContentHeader.js';
+import Properties from './Tabs/Properties.js';
+import ContentTab from './Tabs/Content.js';
+import Permissions from './Tabs/Permissions.js';
+import Children from './Tabs/Children.js';
+import History from './Tabs/History.js';
+import Delete from './Tabs/Delete.js';
+
+const TAB_COMPONENTS = {
+ properties: Properties,
+ content: ContentTab,
+ permissions: Permissions,
+ children: Children,
+ history: History,
+ delete: Delete
+};
+
+const TABS = [
+ { key: 'properties', phrase: 'cms/tabEigenschaften' },
+ { key: 'content', phrase: 'cms/tabInhalt' },
+ { key: 'permissions', phrase: 'cms/tabRechte' },
+ { key: 'children', phrase: 'cms/tabChilds' },
+ { key: 'history', phrase: 'cms/tabHistory' },
+ { key: 'delete', phrase: 'cms/tabLoeschen' }
+];
+
+export default {
+ name: 'CmsAdmin',
+ components: {
+ 'cms-tree': ContentTree,
+ 'cms-content-header': ContentHeader,
+ ...TAB_COMPONENTS
+ },
+ props: {
+ cmsRoot: String,
+ authUid: String,
+ defaultLanguage: { type: String, default: 'German' }
+ },
+ data() {
+ return {
+ contentId: null,
+ sprache: null,
+ version: null,
+ tab: 'properties',
+ contentInfo: null,
+ treeKey: 0
+ };
+ },
+ computed: {
+ activeTabComponent() {
+ return TAB_COMPONENTS[this.tab] || null;
+ }
+ },
+ watch: {
+ '$route.params': {
+ handler(params) {
+ const id = params.content_id ? Number(params.content_id) : null;
+ const sprache = params.sprache || null;
+ const version = params.version ? Number(params.version) : null;
+ const tab = params.tab || 'properties';
+
+ if (id !== this.contentId || sprache !== this.sprache
+ || version !== this.version || tab !== this.tab) {
+ this.contentId = id;
+ this.sprache = sprache;
+ this.version = version;
+ this.tab = tab;
+
+ if (id !== null && this.contentInfo?.content_id !== id) {
+ this.loadContentInfo();
+ }
+ }
+ },
+ immediate: true
+ }
+ },
+ methods: {
+ loadContentInfo() {
+ if (this.contentId === null) {
+ this.contentInfo = null;
+ return;
+ }
+ this.$api
+ .call(ApiCmsAdmin.getContent(this.contentId))
+ .then(result => {
+ this.contentInfo = result.data;
+ });
+ },
+
+ selectContent(content_id) {
+ if (content_id === null) {
+ this.contentId = null;
+ this.sprache = null;
+ this.version = null;
+ this.tab = 'properties';
+ this.contentInfo = null;
+ this.$router.push({ name: 'index' });
+ return;
+ }
+
+ this.contentId = content_id;
+ this.$api
+ .call(ApiCmsAdmin.getContent(content_id))
+ .then(result => {
+ this.contentInfo = result.data;
+ this.preselectLanguageVersion();
+ this.pushRoute();
+ });
+ },
+
+ selectTab(tab) {
+ this.tab = tab;
+ this.pushRoute();
+ },
+
+ selectLanguageVersion(sprache, version) {
+ this.sprache = sprache;
+ this.version = version;
+ this.pushRoute();
+ },
+
+ reloadTree() {
+ this.treeKey++;
+ },
+
+ preselectLanguageVersion() {
+ if (!this.contentInfo) return;
+
+ const languages = this.contentInfo.languages || [];
+ const versions = this.contentInfo.versions || {};
+
+ let lang = languages.includes(this.defaultLanguage)
+ ? this.defaultLanguage
+ : languages[0] || null;
+
+ let ver = null;
+ if (lang && versions[lang] && versions[lang].length) {
+ ver = Math.max(...versions[lang]);
+ }
+
+ this.sprache = lang;
+ this.version = ver;
+ },
+
+ pushRoute() {
+ if (this.contentId === null) {
+ this.$router.push({ name: 'index' });
+ return;
+ }
+ this.$router.push({
+ name: 'content',
+ params: {
+ content_id: this.contentId,
+ sprache: this.sprache,
+ version: this.version,
+ tab: this.tab
+ }
+ });
+ }
+ },
+ template: `
+
+
+
+
+
+
+ {{ $p.t('cms/keinEintragGewaehlt') }}
+
+
+ `,
+ created() {
+ this.tabs = TABS;
+ }
+};
diff --git a/public/js/components/Cms/Form/FieldString.js b/public/js/components/Cms/Form/FieldString.js
new file mode 100644
index 000000000..edaa9c456
--- /dev/null
+++ b/public/js/components/Cms/Form/FieldString.js
@@ -0,0 +1,25 @@
+export default {
+ name: 'FieldString',
+ props: {
+ modelValue: { type: String, default: '' },
+ label: String,
+ required: Boolean,
+ disabled: Boolean,
+ type: { type: String, default: 'text' }
+ },
+ emits: ['update:modelValue'],
+ template: `
+
+
+ {{ label }} *
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Form/FieldWysiwyg.js b/public/js/components/Cms/Form/FieldWysiwyg.js
new file mode 100644
index 000000000..823c1d9f6
--- /dev/null
+++ b/public/js/components/Cms/Form/FieldWysiwyg.js
@@ -0,0 +1,104 @@
+export default {
+ name: 'FieldWysiwyg',
+ props: {
+ modelValue: { type: String, default: '' },
+ label: String,
+ required: Boolean,
+ disabled: Boolean
+ },
+ emits: ['update:modelValue'],
+ data() {
+ return {
+ editor: null,
+ editorId: 'cms-wysiwyg-' + Math.random().toString(36).substring(2, 11)
+ };
+ },
+ methods: {
+ getContent() {
+ return this.editor ? this.editor.getContent() : this.modelValue;
+ }
+ },
+ watch: {
+ modelValue(newVal) {
+ if (this.editor && this.editor.getContent() !== newVal) {
+ this.editor.setContent(newVal || '');
+ }
+ }
+ },
+ mounted() {
+ const vm = this;
+ tinymce.init({
+ target: this.$refs.editorArea,
+ height: 500,
+ language: 'de',
+ // valid_elements: '*[*]' permits every tag and attribute. Existing content
+ // contains audio, video and source tags. A stricter setting deletes them silently.
+ valid_elements: '*[*]',
+ forced_root_block: '',
+ style_formats: [
+ { title: 'Contenttabelle', selector: 'table', classes: 'cmstable' },
+ { title: 'Hauptcontent', block: 'div', classes: 'cmscontent' },
+ { title: 'Menuebox', block: 'div', classes: 'menubox' },
+ { title: 'Teambox', block: 'div', classes: 'teambox' },
+ { title: 'Schatteneffekt Grafiken', selector: 'img', classes: 'screenshot_boxshadow' },
+ { title: 'Tablesorter', selector: 'table',
+ classes: 'tablesorter tablesorter_col_0 tablesorter_sort_0' }
+ ],
+ plugins: 'code lists link image table fullscreen paste searchreplace charmap directionality visualblocks anchor hr',
+ toolbar: [
+ 'code | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | styleselect fontsizeselect',
+ 'pastetext | searchreplace | bullist numlist | outdent indent blockquote | undo redo | link unlink anchor image | forecolor backcolor',
+ 'table | hr removeformat visualblocks | subscript superscript | charmap | ltr rtl | fullscreen'
+ ],
+ file_picker_callback: (callback, value, meta) => {
+ if (vm._dmsListener) {
+ window.removeEventListener('message', vm._dmsListener);
+ }
+
+ const url = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cms/tinymce_dms.php';
+ window.open(url, 'DMS', 'width=1400,height=850,scrollbars=1,resizable=1');
+
+ const onMessage = (event) => {
+ if (event.origin !== window.location.origin) return;
+ if (!event.data || event.data.typ !== 'fhc-dms-selection') return;
+ // The relative form dms.php?id=N is mandatory. The legacy view runs under /cms/
+ // and resolves it directly. The CIS4 view replaces it in CmsLib::getContent
+ // with APP_ROOT . 'cms/dms.php'. An absolute URL breaks both paths.
+ callback(event.data.url);
+ window.removeEventListener('message', onMessage);
+ vm._dmsListener = null;
+ };
+ vm._dmsListener = onMessage;
+ window.addEventListener('message', onMessage);
+ },
+ setup: (editor) => {
+ vm.editor = editor;
+ editor.on('init', () => {
+ editor.setContent(vm.modelValue || '');
+ if (vm.disabled) editor.mode.set('readonly');
+ });
+ editor.on('change input', () => {
+ vm.$emit('update:modelValue', editor.getContent());
+ });
+ }
+ });
+ },
+ beforeUnmount() {
+ if (this._dmsListener) {
+ window.removeEventListener('message', this._dmsListener);
+ this._dmsListener = null;
+ }
+ if (this.editor && tinymce.get(this.editor.id)) {
+ tinymce.get(this.editor.id).remove();
+ this.editor = null;
+ }
+ },
+ template: `
+
+
+ {{ label }} *
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Form/XsdForm.js b/public/js/components/Cms/Form/XsdForm.js
new file mode 100644
index 000000000..996800936
--- /dev/null
+++ b/public/js/components/Cms/Form/XsdForm.js
@@ -0,0 +1,72 @@
+import FieldString from './FieldString.js';
+import FieldWysiwyg from './FieldWysiwyg.js';
+
+export default {
+ name: 'XsdForm',
+ components: {
+ 'field-string': FieldString,
+ 'field-wysiwyg': FieldWysiwyg
+ },
+ props: {
+ schema: Object,
+ modelValue: Object,
+ disabled: Boolean
+ },
+ emits: ['update:modelValue'],
+ methods: {
+ updateField(name, value) {
+ this.$emit('update:modelValue', { ...this.modelValue, [name]: value });
+ },
+ collectValues() {
+ const vals = { ...this.modelValue };
+ if (!this.schema?.fields) return vals;
+ for (const field of this.schema.fields) {
+ if (field.type === 'wysiwyg') {
+ const ref = this.$refs['wysiwyg-' + field.name];
+ const comp = Array.isArray(ref) ? ref[0] : ref;
+ if (comp?.getContent) vals[field.name] = comp.getContent();
+ }
+ }
+ return vals;
+ }
+ },
+ template: `
+
+
+
+
+
+
+
+ {{ field.name }} *
+
+
+
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Header/ContentHeader.js b/public/js/components/Cms/Header/ContentHeader.js
new file mode 100644
index 000000000..3b0329f7c
--- /dev/null
+++ b/public/js/components/Cms/Header/ContentHeader.js
@@ -0,0 +1,167 @@
+import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
+import VersionSelect from './VersionSelect.js';
+import LanguageSelect from './LanguageSelect.js';
+
+export default {
+ name: 'CmsContentHeader',
+ components: {
+ 'version-select': VersionSelect,
+ 'sprache-select': LanguageSelect
+ },
+ props: {
+ contentId: Number,
+ sprache: String,
+ version: Number,
+ contentInfo: Object
+ },
+ emits: ['select-language-version', 'reload-content-info', 'reload-tree'],
+ data() {
+ return {
+ titel: '',
+ allSprachen: [],
+ versionDetails: [],
+ newSprache: null
+ };
+ },
+ computed: {
+ languages() {
+ return this.contentInfo?.languages || [];
+ },
+ versions() {
+ if (!this.contentInfo?.versions || !this.sprache) return [];
+ return this.contentInfo.versions[this.sprache] || [];
+ },
+ availableSprachen() {
+ return this.allSprachen.filter(
+ s => !this.languages.includes(s.sprache)
+ );
+ }
+ },
+ watch: {
+ contentId() {
+ this.loadTitel();
+ this.loadVersionDetails();
+ },
+ sprache() {
+ this.loadTitel();
+ this.loadVersionDetails();
+ },
+ version() {
+ this.loadTitel();
+ }
+ },
+ methods: {
+ loadTitel() {
+ if (this.contentId == null || !this.sprache || !this.version) return;
+ this.$api
+ .call(ApiCmsAdmin.getContentsprache(this.contentId, this.sprache, this.version))
+ .then(result => {
+ this.titel = result.data?.titel || '';
+ });
+ },
+
+ loadVersionDetails() {
+ if (this.contentId == null || !this.sprache) {
+ this.versionDetails = [];
+ return;
+ }
+ this.$api
+ .call(ApiCmsAdmin.getVersions(this.contentId, this.sprache))
+ .then(result => {
+ this.versionDetails = result.data || [];
+ });
+ },
+
+ loadSprachen() {
+ this.$api
+ .call(ApiCmsAdmin.getSprachen())
+ .then(result => {
+ this.allSprachen = result.data || [];
+ });
+ },
+
+ selectVersion(ver) {
+ this.$emit('select-language-version', this.sprache, ver);
+ },
+
+ selectLanguage(lang) {
+ const versions = this.contentInfo?.versions?.[lang] || [];
+ const maxVersion = versions.length ? Math.max(...versions) : 1;
+ this.$emit('select-language-version', lang, maxVersion);
+ },
+
+ createVersion() {
+ this.$api
+ .call(ApiCmsAdmin.postVersion(this.contentId, this.sprache))
+ .then(result => {
+ const newVersion = result.data.version;
+ this.$fhcAlert.alertInfo(this.$p.t('cms/neueVersionIstUnsichtbar'));
+ this.$emit('reload-content-info');
+ this.$emit('select-language-version', this.sprache, newVersion);
+ })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ createTranslation() {
+ if (!this.newSprache) return;
+ this.$api
+ .call(ApiCmsAdmin.postTranslation(
+ this.contentId, this.sprache, this.version, this.newSprache
+ ))
+ .then(() => {
+ const lang = this.newSprache;
+ this.newSprache = null;
+ this.$emit('reload-content-info');
+ this.$emit('select-language-version', lang, this.version);
+ })
+ .catch(this.$fhcAlert.handleSystemError);
+ }
+ },
+ mounted() {
+ this.loadSprachen();
+ this.loadTitel();
+ this.loadVersionDetails();
+ },
+ template: `
+
+
+ {{ $p.t('cms/contentId') }}: {{ contentId }}
+ | {{ $p.t('cms/version') }}: {{ version }}
+ | {{ $p.t('cms/sprache') }}: {{ sprache }}
+ | {{ $p.t('cms/titel') }}: {{ titel }}
+
+
+
+
+
+
+
+ {{ $p.t('cms/neueVersionAnlegen') }}
+
+
+
+ {{ $p.t('cms/spracheWaehlen') }}
+ {{ s.bezeichnung }}
+
+ {{ $p.t('cms/uebersetzungAnlegen') }}
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Header/LanguageSelect.js b/public/js/components/Cms/Header/LanguageSelect.js
new file mode 100644
index 000000000..f4b365ef5
--- /dev/null
+++ b/public/js/components/Cms/Header/LanguageSelect.js
@@ -0,0 +1,20 @@
+export default {
+ name: 'SpracheSelect',
+ props: {
+ languages: { type: Array, default: () => [] },
+ sprache: String
+ },
+ emits: ['select-language'],
+ template: `
+
+ {{ lang }}
+
+ `
+};
diff --git a/public/js/components/Cms/Header/VersionSelect.js b/public/js/components/Cms/Header/VersionSelect.js
new file mode 100644
index 000000000..227a8e61f
--- /dev/null
+++ b/public/js/components/Cms/Header/VersionSelect.js
@@ -0,0 +1,38 @@
+export default {
+ name: 'VersionSelect',
+ props: {
+ versions: { type: Array, default: () => [] },
+ versionDetails: { type: Array, default: () => [] },
+ version: Number
+ },
+ emits: ['select-version'],
+ computed: {
+ sichtbarMap() {
+ const map = {};
+ for (const v of this.versionDetails) {
+ map[v.version] = v.sichtbar;
+ }
+ return map;
+ }
+ },
+ methods: {
+ isSichtbar(ver) {
+ return this.sichtbarMap[ver] !== false;
+ }
+ },
+ template: `
+
+ {{ ver }}
+
+ `
+};
diff --git a/public/js/components/Cms/Preview.js b/public/js/components/Cms/Preview.js
new file mode 100644
index 000000000..40eee3909
--- /dev/null
+++ b/public/js/components/Cms/Preview.js
@@ -0,0 +1,65 @@
+export default {
+ name: 'CmsPreview',
+ props: {
+ contentId: Number,
+ sprache: String,
+ version: Number,
+ templateKurzbz: String,
+ sichtbar: Boolean
+ },
+ computed: {
+ // The preview uses the legacy renderer cms/content.php. It reads xslt_xhtml, which
+ // holds a complete HTML document for all eleven templates. The CIS4 route reads
+ // xslt_xhtml_c4, where four templates hold a whole document instead of a fragment.
+ previewUrl() {
+ return FHC_JS_DATA_STORAGE_OBJECT.app_root
+ + 'cms/content.php?content_id=' + this.contentId
+ + '&version=' + this.version
+ + '&sprache=' + this.sprache
+ + '&sichtbar=';
+ },
+ // The CIS4 route takes content_id only. Its Vue route is
+ // /CisVue/Cms/Content/:content_id, and extra segments hit the catch-all, which
+ // redirects to the dashboard. CIS4 picks the version itself and the language from
+ // the viewer profile.
+ cis4Url() {
+ return FHC_JS_DATA_STORAGE_OBJECT.app_root
+ + FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ + '/CisVue/Cms/content/' + this.contentId;
+ },
+ // The redirect template navigates away with JavaScript. In an iframe this can close
+ // the admin page. Therefore this case uses a link and not an iframe.
+ isRedirect() {
+ return this.templateKurzbz === 'redirect';
+ }
+ },
+ methods: {
+ neuLaden() {
+ if (this.$refs.previewFrame) {
+ this.$refs.previewFrame.src = this.previewUrl + '&t=' + Date.now();
+ }
+ }
+ },
+ template: `
+
+ `
+};
diff --git a/public/js/components/Cms/Tabs/Children.js b/public/js/components/Cms/Tabs/Children.js
new file mode 100644
index 000000000..f5f9c1043
--- /dev/null
+++ b/public/js/components/Cms/Tabs/Children.js
@@ -0,0 +1,179 @@
+import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
+
+export default {
+ name: 'CmsChildren',
+ props: {
+ contentId: Number,
+ sprache: String,
+ version: Number,
+ contentInfo: Object
+ },
+ emits: ['reload-content-info', 'reload-tree', 'select-content'],
+ data() {
+ return {
+ childs: [],
+ possibleChilds: [],
+ selectedChild: ''
+ };
+ },
+ computed: {
+ // tbl_contentchild has no unique index. The server permits a duplicate assignment.
+ // Therefore the select list hides the children that are already assigned.
+ availableChilds() {
+ const assigned = new Set(this.childs.map(c => c.child_content_id));
+ return this.possibleChilds.filter(c => !assigned.has(c.content_id));
+ }
+ },
+ watch: {
+ contentId() { this.loadAll(); },
+ sprache() { this.loadAll(); }
+ },
+ methods: {
+ loadAll() {
+ this.loadChilds();
+ this.loadPossibleChilds();
+ },
+
+ loadChilds() {
+ if (this.contentId == null || !this.sprache) return;
+ this.$api
+ .call(ApiCmsAdmin.getChilds(this.contentId, this.sprache))
+ .then(result => { this.childs = result.data || []; })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ loadPossibleChilds() {
+ if (this.contentId == null || !this.sprache) return;
+ this.$api
+ .call(ApiCmsAdmin.getPossibleChilds(this.contentId, this.sprache))
+ .then(result => {
+ this.possibleChilds = result.data || [];
+ this.selectedChild = '';
+ })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ childLabel(c) {
+ const titel = c.titel || this.$p.t('cms/ohneTitel');
+ return titel + ' (' + c.content_id + ')';
+ },
+
+ async addChild() {
+ if (!this.selectedChild) return;
+ try {
+ await this.$api.call(
+ ApiCmsAdmin.postChild(this.contentId, this.selectedChild)
+ );
+ this.loadAll();
+ this.$emit('reload-tree');
+ } catch (e) {
+ this.$fhcAlert.handleSystemError(e);
+ }
+ },
+
+ async removeChild(contentchild_id) {
+ const confirmed = await this.$fhcAlert.confirm({
+ message: this.$p.t('cms/zuordnungEntfernenBestaetigen')
+ });
+ if (!confirmed) return;
+ try {
+ await this.$api.call(ApiCmsAdmin.deleteChild(contentchild_id));
+ this.loadAll();
+ this.$emit('reload-tree');
+ } catch (e) {
+ this.$fhcAlert.handleSystemError(e);
+ }
+ },
+
+ async moveSort(contentchild_id, direction) {
+ try {
+ await this.$api.call(
+ ApiCmsAdmin.putChildSort(contentchild_id, direction)
+ );
+ this.loadChilds();
+ this.$emit('reload-tree');
+ } catch (e) {
+ this.$fhcAlert.handleSystemError(e);
+ }
+ },
+
+ onChildClick(child_content_id) {
+ this.$emit('select-content', child_content_id);
+ },
+
+ isFirst(index) {
+ return index === 0;
+ },
+
+ isLast(index) {
+ return index === this.childs.length - 1;
+ }
+ },
+ mounted() {
+ this.loadAll();
+ },
+ template: `
+
+
{{ $p.t('cms/folgendeEintraegeSindUntergeordnet') }}
+
+
+
+
+
+ {{ $p.t('cms/sortierung') }}
+ ID
+ {{ $p.t('cms/titel') }}
+
+
+
+
+
+
+ {{ child.sort }}
+
+
+
+
+
+
+
+ {{ child.child_content_id }}
+
+
+ {{ child.titel || $p.t('cms/ohneTitel') }}
+
+
+
+
+ {{ $p.t('cms/entfernen') }}
+
+
+
+
+
+
+
+
+ {{ $p.t('cms/keineUntereintraege') }}
+
+
+
+
+
+
+ {{ childLabel(c) }}
+
+
+
+ {{ $p.t('cms/hinzufuegen') }}
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Tabs/Content.js b/public/js/components/Cms/Tabs/Content.js
new file mode 100644
index 000000000..fee4897aa
--- /dev/null
+++ b/public/js/components/Cms/Tabs/Content.js
@@ -0,0 +1,148 @@
+import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
+import XsdForm from '../Form/XsdForm.js';
+import CmsPreview from '../Preview.js';
+
+export default {
+ name: 'CmsContent',
+ components: {
+ 'xsd-form': XsdForm,
+ 'cms-preview': CmsPreview
+ },
+ props: {
+ contentId: Number,
+ sprache: String,
+ version: Number,
+ contentInfo: Object
+ },
+ emits: ['reload-content-info', 'reload-tree', 'select-content'],
+ data() {
+ return {
+ schema: null,
+ values: {},
+ sperre: null,
+ sichtbar: true,
+ loading: false,
+ saving: false
+ };
+ },
+ computed: {
+ lockState() {
+ if (!this.sperre || this.sperre.gesperrt_uid === null) return 'free';
+ if (this.sperre.own) return 'own';
+ return 'foreign';
+ }
+ },
+ watch: {
+ contentId() { this.loadFormData(); },
+ sprache() { this.loadFormData(); },
+ version() { this.loadFormData(); }
+ },
+ methods: {
+ loadFormData() {
+ if (this.contentId == null || !this.sprache || !this.version) return;
+ this.loading = true;
+ Promise.all([
+ this.$api.call(ApiCmsAdmin.getFormData(this.contentId, this.sprache, this.version)),
+ this.$api.call(ApiCmsAdmin.getContentsprache(this.contentId, this.sprache, this.version))
+ ])
+ .then(([formResult, spracheResult]) => {
+ const d = formResult.data;
+ this.schema = d.schema;
+ this.values = d.values || {};
+ this.sperre = d.sperre;
+ this.sichtbar = !!spracheResult.data.sichtbar;
+ })
+ .catch(this.$fhcAlert.handleSystemError)
+ .finally(() => { this.loading = false; });
+ },
+
+ lock() {
+ this.$api
+ .call(ApiCmsAdmin.postLock(this.sperre.contentsprache_id))
+ .then(() => { this.loadFormData(); })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ // LEGACY-QUIRK: Releasing a lock releases all locks of this user, not just this page's. See Q1.
+ unlock() {
+ this.$api
+ .call(ApiCmsAdmin.deleteLock(this.sperre.contentsprache_id))
+ .then(() => { this.loadFormData(); })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ forceUnlock() {
+ this.$api
+ .call(ApiCmsAdmin.deleteLockForced(this.sperre.contentsprache_id))
+ .then(() => { this.loadFormData(); })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ save() {
+ const freshValues = this.$refs.xsdForm
+ ? this.$refs.xsdForm.collectValues()
+ : this.values;
+ this.saving = true;
+ this.$api
+ .call(ApiCmsAdmin.putFormData(
+ this.contentId, this.sprache, this.version, freshValues
+ ))
+ .then(() => {
+ this.$fhcAlert.alertSuccess(this.$p.t('cms/gespeichert'));
+ if (this.$refs.preview) this.$refs.preview.neuLaden();
+ })
+ .catch(this.$fhcAlert.handleSystemError)
+ .finally(() => { this.saving = false; });
+ }
+ },
+ mounted() {
+ this.loadFormData();
+ },
+ template: `
+
+
+
+
+ {{ $p.t('cms/zurBearbeitungSperren') }}
+
+
+
+
+
+ {{ $p.t('cms/freigabeGibtAlleSperrenFrei') }}
+
+
+ {{ $p.t('cms/sperreFreigeben') }}
+
+
+
+ {{ $p.t('cms/speichern') }}
+
+
+
+
+
+ {{ $p.t('cms/gesperrtVonSeit', { uid: sperre.gesperrt_uid, start: sperre.start }) }}
+
+
+ {{ $p.t('cms/freigabeErzwingen') }}
+
+
+
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Tabs/Delete.js b/public/js/components/Cms/Tabs/Delete.js
new file mode 100644
index 000000000..dbfd58345
--- /dev/null
+++ b/public/js/components/Cms/Tabs/Delete.js
@@ -0,0 +1,142 @@
+import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
+
+export default {
+ name: 'CmsDelete',
+ props: {
+ contentId: Number,
+ sprache: String,
+ version: Number,
+ contentInfo: Object
+ },
+ emits: ['reload-content-info', 'reload-tree', 'select-content'],
+ data() {
+ return {
+ usages: [],
+ loadingUsage: true
+ };
+ },
+ computed: {
+ hasRestrict() {
+ return this.usages.some(u => u.table !== 'campus.tbl_infoscreen_content');
+ },
+ // Request to T07: forward defaultLanguage prop to tabs. Using 'German' as fallback.
+ isLastVersion() {
+ if (!this.contentInfo?.versions || !this.sprache) return false;
+ const defaultLang = 'German';
+ const versions = this.contentInfo.versions[this.sprache];
+ return this.sprache === defaultLang && versions && versions.length === 1;
+ },
+ totalVersionCount() {
+ if (!this.contentInfo?.versions) return 0;
+ return Object.values(this.contentInfo.versions)
+ .reduce((sum, vers) => sum + vers.length, 0);
+ }
+ },
+ watch: {
+ contentId() { this.loadUsage(); }
+ },
+ methods: {
+ loadUsage() {
+ if (this.contentId == null) return;
+ this.loadingUsage = true;
+ this.$api
+ .call(ApiCmsAdmin.getUsage(this.contentId))
+ .then(result => {
+ this.usages = result.data || [];
+ })
+ .finally(() => { this.loadingUsage = false; });
+ },
+
+ deleteVersion() {
+ this.$fhcAlert.confirm({
+ message: this.$p.t('cms/versionLoeschenBestaetigen'),
+ acceptLabel: this.$p.t('ui/loeschen'),
+ acceptClass: 'p-button-danger'
+ }).then(confirmed => {
+ if (!confirmed) return;
+ return this.$api.call(
+ ApiCmsAdmin.deleteContentsprache(this.contentId, this.sprache, this.version)
+ );
+ }).then(result => {
+ if (!result) return;
+ this.$emit('reload-content-info');
+ }).catch(this.$fhcAlert.handleSystemError);
+ },
+
+ deleteContent() {
+ this.$fhcAlert.confirm({
+ message: this.$p.t('cms/contentLoeschenBestaetigen', {
+ sprachen: this.contentInfo.languages.length,
+ versionen: this.totalVersionCount
+ }),
+ acceptLabel: this.$p.t('ui/loeschen'),
+ acceptClass: 'p-button-danger'
+ }).then(confirmed => {
+ if (!confirmed) return;
+ return this.$api.call(ApiCmsAdmin.deleteContent(this.contentId));
+ }).then(result => {
+ if (!result) return;
+ this.$emit('reload-tree');
+ this.$nextTick(() => {
+ this.$emit('select-content', null);
+ });
+ }).catch(this.$fhcAlert.handleSystemError);
+ }
+ },
+ mounted() {
+ this.loadUsage();
+ },
+ // Five tables prevent the deletion with ON DELETE RESTRICT. tbl_infoscreen_content
+ // deletes its own rows with CASCADE. The legacy code shows neither case and hides the
+ // error. See Q5 in the contract.
+ template: `
+
+
{{ $p.t('cms/dieseVersionLoeschen') }}
+
+
+ {{ $p.t('cms/letzteVersionNichtLoeschbar') }}
+
+
+
+ {{ $p.t('cms/dieseVersionLoeschen') }}
+
+
+
+
+
+
{{ $p.t('cms/ganzenContentLoeschen') }}
+
+
+
+
+ {{ $p.t('cms/keineVerwendungGefunden') }}
+
+
+
+ {{ $p.t('cms/contentWirdVerwendetIn') }}
+
+
+
+ {{ u.label }}
+
+ {{ $p.t('cms/wirdMitgeloescht') }}
+
+
+ {{ $p.t('cms/verhindertLoeschen') }}
+
+
+
+
+
+
+
+ {{ $p.t('cms/ganzenContentLoeschen') }}
+
+
+ `
+};
diff --git a/public/js/components/Cms/Tabs/History.js b/public/js/components/Cms/Tabs/History.js
new file mode 100644
index 000000000..80119e9ac
--- /dev/null
+++ b/public/js/components/Cms/Tabs/History.js
@@ -0,0 +1,65 @@
+import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
+
+export default {
+ name: 'CmsHistory',
+ props: {
+ contentId: Number,
+ sprache: String,
+ version: Number,
+ contentInfo: Object
+ },
+ emits: ['reload-content-info', 'reload-tree', 'select-content'],
+ data() {
+ return {
+ versions: []
+ };
+ },
+ watch: {
+ contentId() { this.loadVersions(); },
+ sprache() { this.loadVersions(); }
+ },
+ methods: {
+ loadVersions() {
+ if (this.contentId == null || !this.sprache) return;
+ this.$api
+ .call(ApiCmsAdmin.getVersions(this.contentId, this.sprache))
+ .then(result => {
+ this.versions = result.data || [];
+ })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ formatDate(dateStr) {
+ if (!dateStr) return '';
+ try {
+ const dt = luxon.DateTime.fromSQL(dateStr);
+ return dt.isValid ? dt.toFormat('dd.MM.yyyy') : dateStr;
+ } catch (e) {
+ return dateStr;
+ }
+ }
+ },
+ mounted() {
+ this.loadVersions();
+ },
+ template: `
+
+
{{ $p.t('cms/versionen') }}
+
+
+
+ {{ $p.t('cms/version') }} {{ v.version }}
+
+ {{ $p.t('cms/unsichtbar') }}
+
+
+
{{ $p.t('cms/erstelltAmVon', { amum: formatDate(v.insertamum), von: v.insertvon }) }}
+
+ {{ $p.t('cms/letzteAenderungVonAm', { von: v.updatevon, amum: formatDate(v.updateamum) }) }}
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Tabs/Permissions.js b/public/js/components/Cms/Tabs/Permissions.js
new file mode 100644
index 000000000..f9a7358cb
--- /dev/null
+++ b/public/js/components/Cms/Tabs/Permissions.js
@@ -0,0 +1,132 @@
+import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
+
+export default {
+ name: 'CmsPermissions',
+ props: {
+ contentId: Number,
+ sprache: String,
+ version: Number,
+ contentInfo: Object
+ },
+ emits: ['reload-content-info', 'reload-tree', 'select-content'],
+ data() {
+ return {
+ gruppen: [],
+ allGruppen: [],
+ selectedGruppe: '',
+ allGruppenLoaded: false
+ };
+ },
+ computed: {
+ availableGruppen() {
+ const assigned = new Set(this.gruppen.map(g => g.gruppe_kurzbz));
+ return this.allGruppen.filter(g => !assigned.has(g.gruppe_kurzbz));
+ }
+ },
+ watch: {
+ contentId() { this.loadGruppen(); }
+ },
+ methods: {
+ loadGruppen() {
+ if (this.contentId == null) return;
+ this.$api
+ .call(ApiCmsAdmin.getGruppen(this.contentId))
+ .then(result => {
+ this.gruppen = result.data || [];
+ this.selectedGruppe = '';
+ })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ loadAllGruppen() {
+ if (this.allGruppenLoaded) return;
+ this.$api
+ .call(ApiCmsAdmin.getAllGruppen())
+ .then(result => {
+ this.allGruppen = result.data || [];
+ this.allGruppenLoaded = true;
+ })
+ .catch(this.$fhcAlert.handleSystemError);
+ },
+
+ async addGruppe() {
+ if (!this.selectedGruppe) return;
+ try {
+ await this.$api.call(
+ ApiCmsAdmin.postGruppe(this.contentId, this.selectedGruppe)
+ );
+ this.loadGruppen();
+ this.$emit('reload-tree');
+ } catch (e) {
+ this.$fhcAlert.handleSystemError(e);
+ }
+ },
+
+ async removeGruppe(gruppe_kurzbz) {
+ if (this.gruppen.length <= 1) {
+ const confirmed = await this.$fhcAlert.confirm({
+ message: this.$p.t('cms/letzteGruppeEntfernenBestaetigen')
+ });
+ if (!confirmed) return;
+ }
+ try {
+ await this.$api.call(
+ ApiCmsAdmin.deleteGruppe(this.contentId, gruppe_kurzbz)
+ );
+ this.loadGruppen();
+ this.$emit('reload-tree');
+ } catch (e) {
+ this.$fhcAlert.handleSystemError(e);
+ }
+ }
+ },
+ mounted() {
+ this.loadAllGruppen();
+ this.loadGruppen();
+ },
+ template: `
+
+
+ {{ $p.t('cms/folgendeGruppenDuerfenAnsehen') }}
+
+
+
+ {{ $p.t('cms/gruppe') }}
+ {{ $p.t('cms/bezeichnung') }}
+
+
+
+
+
+ {{ g.gruppe_kurzbz }}
+ {{ g.bezeichnung }}
+
+
+ {{ $p.t('cms/entfernen') }}
+
+
+
+
+
+
+
+
+ {{ $p.t('cms/seiteDarfVonAllenAngezeigtWerden') }}
+
+
+
+
+
+
+ {{ g.gruppe_kurzbz }} — {{ g.bezeichnung }}
+
+
+
+ {{ $p.t('cms/hinzufuegen') }}
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Tabs/Properties.js b/public/js/components/Cms/Tabs/Properties.js
new file mode 100644
index 000000000..34179eb0a
--- /dev/null
+++ b/public/js/components/Cms/Tabs/Properties.js
@@ -0,0 +1,184 @@
+import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
+
+export default {
+ name: 'CmsProperties',
+ props: {
+ contentId: Number,
+ sprache: String,
+ version: Number,
+ contentInfo: Object
+ },
+ emits: ['reload-content-info', 'reload-tree', 'select-content'],
+ data() {
+ return {
+ titel: '',
+ sichtbar: false,
+ template_kurzbz: '',
+ oe_kurzbz: '',
+ aktiv: false,
+ menu_open: false,
+ beschreibung: '',
+ templates: [],
+ organisationseinheiten: [],
+ templateChanged: false,
+ saving: false
+ };
+ },
+ watch: {
+ contentId() { this.loadData(); },
+ sprache() { this.loadData(); },
+ version() { this.loadData(); },
+ contentInfo() { this.loadContentFields(); }
+ },
+ methods: {
+ loadData() {
+ this.templateChanged = false;
+ if (this.contentId == null || !this.sprache || !this.version) return;
+
+ this.$api
+ .call(ApiCmsAdmin.getContentsprache(this.contentId, this.sprache, this.version))
+ .then(result => {
+ const d = result.data;
+ this.titel = d.titel || '';
+ this.sichtbar = !!d.sichtbar;
+ });
+
+ this.loadContentFields();
+ },
+
+ loadContentFields() {
+ if (!this.contentInfo) return;
+ this.template_kurzbz = this.contentInfo.template_kurzbz || '';
+ this.oe_kurzbz = this.contentInfo.oe_kurzbz || '';
+ this.aktiv = !!this.contentInfo.aktiv;
+ this.menu_open = !!this.contentInfo.menu_open;
+ this.beschreibung = this.contentInfo.beschreibung || '';
+ },
+
+ loadLookups() {
+ this.$api
+ .call(ApiCmsAdmin.getTemplates())
+ .then(result => { this.templates = result.data || []; });
+ this.$api
+ .call(ApiCmsAdmin.getOrganisationseinheiten())
+ .then(result => { this.organisationseinheiten = result.data || []; });
+ },
+
+ onTemplateChange() {
+ this.templateChanged = true;
+ },
+
+ oeLabel(oe) {
+ let label = oe.organisationseinheittyp_kurzbz + ' ' + oe.bezeichnung;
+ if (!oe.aktiv) label += ' (' + this.$p.t('cms/inaktiv') + ')';
+ return label;
+ },
+
+ save() {
+ this.saving = true;
+ this.$api
+ .call(ApiCmsAdmin.putProperties(
+ this.contentId, this.sprache, this.version,
+ this.template_kurzbz, this.oe_kurzbz,
+ this.aktiv, this.menu_open, this.beschreibung,
+ this.titel, this.sichtbar
+ ))
+ .then(() => {
+ this.$fhcAlert.alertSuccess(this.$p.t('cms/gespeichert'));
+ this.$emit('reload-content-info');
+ this.$emit('reload-tree');
+ })
+ .catch(this.$fhcAlert.handleSystemError)
+ .finally(() => { this.saving = false; });
+ }
+ },
+ mounted() {
+ this.loadLookups();
+ this.loadData();
+ },
+ template: `
+
+
{{ $p.t('cms/giltFuerAlleSprachen') }}
+
+
{{ $p.t('cms/vorlage') }}
+
+
+ {{ t.bezeichnung }}
+
+
+ {{ $p.t('cms/vorlageWarnung') }}
+
+
+
+
+
{{ $p.t('cms/organisationseinheit') }}
+
+
+ {{ oeLabel(oe) }}
+
+
+
+
+
+
+
+
+ {{ $p.t('cms/aktiv') }}
+
+
+
+
+
+
+
+
+
+ {{ $p.t('cms/menuOpen') }}
+
+
+
+
+
+
{{ $p.t('cms/beschreibung') }}
+
+
+
+
+
+
+
+
{{ $p.t('cms/giltFuerDieseVersion') }}
+
+
{{ $p.t('cms/titel') }}
+
+
+
+
+
+
+
+
+
+ {{ $p.t('cms/sichtbar') }}
+
+
+
+
+
+
+
+
+
+ {{ $p.t('cms/speichern') }}
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Tree/ContentTree.js b/public/js/components/Cms/Tree/ContentTree.js
new file mode 100644
index 000000000..0bd83e54f
--- /dev/null
+++ b/public/js/components/Cms/Tree/ContentTree.js
@@ -0,0 +1,200 @@
+import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
+import TreeFilter from './TreeFilter.js';
+import MenuSwitch from './MenuSwitch.js';
+
+const TreeNode = {
+ name: 'CmsTreeNode',
+ inject: ['treeSelect', 'treeToggle'],
+ props: {
+ node: Object,
+ activeContentId: Number,
+ expandedKeys: Object,
+ flat: Boolean
+ },
+ computed: {
+ hasChildren() {
+ return this.node.children && this.node.children.length > 0;
+ },
+ isExpanded() {
+ return !!this.expandedKeys[this.node.content_id];
+ },
+ isActive() {
+ return this.activeContentId != null
+ && this.node.content_id === this.activeContentId;
+ },
+ labelStyle() {
+ const s = {};
+ if (this.isActive) {
+ s.fontWeight = 'bold';
+ s.background = '#e8f0fe';
+ s.borderRadius = '3px';
+ s.padding = '0 4px';
+ }
+ if (!this.node.aktiv) {
+ s.opacity = '0.5';
+ }
+ return s;
+ }
+ },
+ template: `
+
+
+
+
+
+
+ {{ node.titel }} ({{ node.content_id }})
+ {{ node.titel }} ({{ node.content_id }})
+
+
+ `
+};
+
+export default {
+ name: 'CmsTree',
+ components: {
+ 'cms-tree-node': TreeNode,
+ 'tree-filter': TreeFilter,
+ 'menu-switch': MenuSwitch
+ },
+ props: {
+ activeContentId: Number
+ },
+ emits: ['select-content', 'entry-created'],
+ data() {
+ const stored = localStorage.getItem('cms/menu');
+ return {
+ menu: (stored === 'content' || stored === 'news') ? stored : 'content',
+ filter: '',
+ nodes: [],
+ loading: false,
+ expandedKeys: {}
+ };
+ },
+ provide() {
+ return {
+ treeSelect: (node) => { this.selectNode(node); },
+ treeToggle: (node) => { this.toggleNode(node); }
+ };
+ },
+ watch: {
+ menu() {
+ this.loadTree();
+ },
+ activeContentId(id) {
+ if (id != null) {
+ this.expandPathTo(id);
+ }
+ }
+ },
+ methods: {
+ loadTree() {
+ this.loading = true;
+ this.$api
+ .call(ApiCmsAdmin.getTree(this.menu, this.filter))
+ .then(result => {
+
+ this.nodes = result.data;
+ this.expandedKeys = {};
+ if (this.activeContentId != null) {
+ this.expandPathTo(this.activeContentId);
+ }
+ this.loading = false;
+ });
+ },
+ expandPathTo(targetId) {
+ const search = (nodes) => {
+ for (const node of nodes) {
+ if (node.content_id === targetId) return true;
+ if (node.children && node.children.length) {
+ if (search(node.children)) {
+ this.expandedKeys[node.content_id] = true;
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+ search(this.nodes);
+ },
+ toggleNode(node) {
+ if (this.expandedKeys[node.content_id]) {
+ delete this.expandedKeys[node.content_id];
+ } else {
+ this.expandedKeys[node.content_id] = true;
+ }
+ },
+ selectNode(node) {
+ if (node.entitled) {
+ this.$emit('select-content', node.content_id);
+ }
+ },
+ onFilter(text) {
+ this.filter = text;
+ this.loadTree();
+ },
+ createEntry(parentId) {
+ this.$api
+ .call(ApiCmsAdmin.postContent(parentId))
+ .then(result => {
+ const newId = result.data.content_id;
+ this.$emit('entry-created');
+ this.$emit('select-content', newId);
+ });
+ }
+ },
+ mounted() {
+ this.loadTree();
+ },
+ template: `
+
+
+
+
+
+ {{ $p.t('cms/neuenEintragHinzufuegen') }}
+
+
+ {{ $p.t('cms/neuenChildEintragHinzufuegen') }}
+
+
+
+
+
+
+ {{ filter ? $p.t('cms/keineTreffer') : $p.t('cms/keineEintraege') }}
+
+
+
+ `
+};
diff --git a/public/js/components/Cms/Tree/MenuSwitch.js b/public/js/components/Cms/Tree/MenuSwitch.js
new file mode 100644
index 000000000..ae31f2be9
--- /dev/null
+++ b/public/js/components/Cms/Tree/MenuSwitch.js
@@ -0,0 +1,35 @@
+export default {
+ name: 'CmsMenuSwitch',
+ props: {
+ modelValue: { type: String, default: 'content' }
+ },
+ emits: ['update:modelValue'],
+ methods: {
+ select(value) {
+ localStorage.setItem('cms/menu', value);
+ this.$emit('update:modelValue', value);
+ }
+ },
+ mounted() {
+ // The legacy code keeps this choice in $_SESSION['cms/menu']. The Vue client uses
+ // localStorage. The effect for the editor is the same.
+ const stored = localStorage.getItem('cms/menu');
+ if ((stored === 'content' || stored === 'news') && stored !== this.modelValue) {
+ this.$emit('update:modelValue', stored);
+ }
+ },
+ template: `
+
+
+ {{ $p.t('cms/menuContent') }}
+
+
+ {{ $p.t('cms/menuNews') }}
+
+
+ `
+};
diff --git a/public/js/components/Cms/Tree/TreeFilter.js b/public/js/components/Cms/Tree/TreeFilter.js
new file mode 100644
index 000000000..3012c668f
--- /dev/null
+++ b/public/js/components/Cms/Tree/TreeFilter.js
@@ -0,0 +1,26 @@
+export default {
+ name: 'CmsTreeFilter',
+ emits: ['filter'],
+ data() {
+ return {
+ text: ''
+ };
+ },
+ methods: {
+ submit() {
+ this.$emit('filter', this.text);
+ }
+ },
+ template: `
+
+
+
+
+
+
+ `
+};
diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php
index 280a1cf42..dfbbe711a 100644
--- a/system/phrasesupdate.php
+++ b/system/phrasesupdate.php
@@ -62411,6 +62411,1727 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
+ //******************* CORE/cms
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'tabEigenschaften',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Eigenschaften',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Properties',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'tabInhalt',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Inhalt',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Content',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'tabRechte',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Rechte',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Permissions',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'tabChilds',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Childs',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Children',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'tabHistory',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'History',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'History',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'tabLoeschen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Löschen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Delete',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keinEintragGewaehlt',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Wählen Sie links einen Eintrag aus.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Select an entry on the left.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'speichern',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Speichern',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Save',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'hinzufuegen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Hinzufügen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Add',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'entfernen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Entfernen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Remove',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'abbrechen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Abbrechen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Cancel',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'gespeichert',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Die Daten wurden gespeichert.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The data was saved.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'menuContent',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Content',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Content',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'menuNews',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'News',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'News',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'filterPlatzhalter',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Suchtext oder Content-ID',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Search text or content ID',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'neuenEintragHinzufuegen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Neuen Eintrag hinzufügen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Add new entry',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'neuenChildEintragHinzufuegen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Neuen Child-Eintrag hinzufügen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Add new child entry',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keineTreffer',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Kein Eintrag passt zum Filter.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'No entry matches the filter.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keineEintraege',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Es sind keine Einträge vorhanden.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'There are no entries.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'neuerEintrag',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Neuer Eintrag',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'New entry',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'neueVersionAnlegen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Neue Version anlegen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Create new version',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'neueVersionIstUnsichtbar',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Die neue Version ist unsichtbar. Schalten Sie sie in den Eigenschaften sichtbar.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The new version is invisible. Set it visible in the properties.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'uebersetzungAnlegen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Übersetzung anlegen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Create translation',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'uebersetzungIn',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Übersetzung in',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Translation into',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'giltFuerAlleSprachen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Gilt für alle Sprachen und Versionen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Applies to all languages and versions',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'giltFuerDieseVersion',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Gilt nur für diese Sprache und Version',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Applies to this language and version only',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'feldVorlage',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Vorlage',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Template',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'feldOrganisationseinheit',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Organisationseinheit',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Organisational unit',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'feldAktiv',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Content aktiv',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Content active',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'feldMenuOpen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Menü offen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Menu open',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'feldBeschreibung',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Beschreibung',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Description',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'feldTitel',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Titel',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Title',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'feldSichtbar',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Version sichtbar',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Version visible',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'vorlageWarnung',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Achtung: Das Ändern der Vorlage kann zum Datenverlust des Contents führen. Ändern Sie die Vorlage nur, wenn Sie wissen, was Sie tun.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Caution: changing the template can destroy the content. Only change the template if you know what you are doing.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'zurBearbeitungSperren',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Zur Bearbeitung sperren',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Lock for editing',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'sperreFreigeben',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Sperre freigeben',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Release lock',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'freigabeErzwingen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Freigabe erzwingen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Force release',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'gesperrtVonSeit',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Dieser Content ist von {uid} seit {start} gesperrt.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This content is locked by {uid} since {start}.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'freigabeGibtAlleSperrenFrei',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Hinweis: Die Freigabe gibt alle Ihre Sperren frei, nicht nur diese Seite.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Note: the release frees all of your locks, not only this page.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'inhaltGespeichert',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Der Inhalt wurde gespeichert.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The content was saved.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'vorschau',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Vorschau',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Preview',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'vorschauInEigenemFenster',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Vorschau in eigenem Fenster öffnen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Open preview in a separate window',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'inCis4Ansehen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'In CIS4 ansehen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'View in CIS4',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'unsichtbarImLivesystem',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Unsichtbar im Livesystem',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Invisible in the live system',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'folgendeGruppenDuerfenAnsehen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Die Mitglieder der folgenden Gruppen dürfen die Seite ansehen:',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Members of these groups may view the page:',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'seiteDarfVonAllenAngezeigtWerden',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Diese Seite darf von allen angezeigt werden.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Everyone may view this page.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'letzteGruppeEntfernenBestaetigen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Dies ist die letzte Gruppe. Nach dem Entfernen darf jede Person die Seite ansehen. Fortfahren?',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This is the last group. After removal everyone may view the page. Continue?',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'gruppeBereitsZugeordnet',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Die Gruppe ist bereits zugeordnet.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The group is already assigned.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'gruppe',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Gruppe',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Group',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'bezeichnung',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Bezeichnung',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Name',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'folgendeEintraegeSindUntergeordnet',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Folgende Einträge sind diesem untergeordnet:',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The following entries are subordinate to this one:',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keineUntereintraege',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Es sind keine Untereinträge vorhanden.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'There are no subordinate entries.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'zuordnungEntfernenBestaetigen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Die Zuordnung wird entfernt. Der Content bleibt erhalten und erscheint danach als Wurzeleintrag. Fortfahren?',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The assignment is removed. The content remains and then appears as a root entry. Continue?',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'ohneTitel',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => '(ohne Titel)',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => '(no title)',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'sortierung',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Sortierung',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Order',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'id',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'ID',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'ID',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'titel',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Titel',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Title',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'nachObenSortieren',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Nach oben sortieren',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Move up',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'nachUntenSortieren',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Nach unten sortieren',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Move down',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'bereitsGanzOben',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Dies ist bereits der oberste Eintrag.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This is already the top entry.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'bereitsGanzUnten',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Dies ist bereits der unterste Eintrag.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This is already the bottom entry.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'versionen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Versionen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Versions',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'version',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Version',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Version',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'unsichtbar',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'unsichtbar',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'invisible',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'erstelltAmVon',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Erstellt am {amum} von {von}',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Created on {amum} by {von}',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'letzteAenderungVonAm',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Letzte Änderung von {von} am {amum}',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Last changed by {von} on {amum}',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'dieseVersionLoeschen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Diese Version löschen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Delete this version',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'ganzenContentLoeschen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Den gesamten Content löschen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Delete the entire content',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'versionLoeschenBestaetigen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Diese Version wird gelöscht. Fortfahren?',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This version will be deleted. Continue?',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'contentLoeschenBestaetigen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Der gesamte Content wird mit {sprachen} Sprachen und {versionen} Versionen gelöscht. Fortfahren?',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The entire content will be deleted, with {sprachen} languages and {versionen} versions. Continue?',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'letzteVersionNichtLoeschbar',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Diese Version kann nicht gelöscht werden. Sie ist die letzte in der Standardsprache.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This version cannot be deleted. It is the last one in the default language.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keineVerwendungGefunden',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Der Content wird nirgends referenziert.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The content is not referenced anywhere.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'contentWirdVerwendetIn',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Der Content wird an folgenden Stellen verwendet:',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The content is used in the following places:',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'wirdMitgeloescht',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'wird mitgelöscht',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'will be deleted too',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'verhindertLoeschen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'verhindert das Löschen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'prevents deletion',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'loeschenNichtMoeglichReferenzen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Der Content kann nicht gelöscht werden. Er wird noch an anderer Stelle verwendet.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The content cannot be deleted. It is still used elsewhere.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keineBerechtigungFuerDiesenEintrag',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Sie haben keine Berechtigung für diesen Eintrag.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'You have no permission for this entry.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keineBerechtigungFuerDieseAktion',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Sie haben keine Berechtigung für diese Aktion.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'You have no permission for this action.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keineBerechtigteOe',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Ihnen ist keine Organisationseinheit zugeordnet.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'No organisational unit is assigned to you.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'versionNichtGefunden',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Diese Version wurde nicht gefunden.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This version was not found.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'vorlageNichtGefunden',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Die Vorlage wurde nicht gefunden.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The template was not found.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'keineVorlageVorhanden',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Es ist keine Vorlage vorhanden.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'No template exists.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'uebersetzungExistiert',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Diese Übersetzung existiert bereits.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This translation already exists.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'speichernFehlgeschlagen',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Das Speichern ist fehlgeschlagen.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Saving failed.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'xsdUngueltig',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Das Schema der Vorlage ist ungültig.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The template schema is invalid.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'cms',
+ 'phrase' => 'xmlUngueltig',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Der erzeugte Inhalt ist kein gültiges XML.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'The generated content is not valid XML.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
);