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) . ']]>'; + } + + $xml .= "\n'; + + 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: ` +
+ + +
+ ` +}; 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: ` +
+ + +
+ ` +}; 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: ` +
+ +
+ ` +}; 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 }} +
+
+
+ + +
+
+ +
+
+ + +
+
+
+ ` +}; 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: ` +
+ +
+ ` +}; 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: ` +
+ +
+ ` +}; 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: ` +
+
+ {{ $p.t('cms/unsichtbarImLivesystem') }} +
+ + + +
+ ` +}; 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/keineUntereintraege') }} +
+ +
+ + +
+
+ ` +}; 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: ` +
+ +
+ ` +}; 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/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/seiteDarfVonAllenAngezeigtWerden') }} +
+ +
+ + +
+
+ ` +}; 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/vorlageWarnung') }} +
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+ +
+
+ +
+ +
{{ $p.t('cms/giltFuerDieseVersion') }}
+
+ +
+ +
+
+
+
+
+ + +
+
+
+ +
+ +
+ +
+
+ ` +}; 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: ` +
    + + +
    + + +
    +
    + +
    +
    + {{ 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: ` +
    + + +
    + ` +}; 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' + ) + ) + ), );