. */ if (!defined('BASEPATH')) exit('No direct script access allowed'); class NewsAdministrationAPI extends FHCAPI_Controller { /** * News API constructor. */ public function __construct() { parent::__construct([ 'getNews' => 'basis/news:r', 'getNewsItem' => 'basis/news:r', 'storeNewsItem' => 'basis/news:w', 'updateNewsItem' => 'basis/news:w', 'deleteNewsItem' => 'basis/news:w' ]); $this->load->model('content/Content_model', 'ContentModel'); $this->load->model('content/Contentsprache_model', 'ContentspracheModel'); $this->load->model('content/News_model', 'NewsModel'); $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); $this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel'); $this->load->model('system/Sprache_model', 'SpracheModel'); $this->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel'); $this->loadPhrases([ 'global' ]); $this->load->library('CmsLib'); $this->load->helper('hlp_sancho_helper'); } //------------------------------------------------------------------------------------------------------------------ // Public methods public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $titel = '', $active = true, $sichtbar = true) { $this->load->library('form_validation'); $this->form_validation->set_data($_GET); $this->form_validation->set_rules('page','Page','required|is_natural'); $this->form_validation->set_rules('page_size', 'PageSize', 'is_natural'); if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); $page = intval($this->input->get('page', true)); $page_size = intval($this->input->get('page_size', true)); $sprache = $this->input->get('sprache', true); if(!$sprache) { $sprache = getUserLanguage(); } // default value for the page_size is 10 $page_size = $page_size ?? 10; $passedSichtbar = $this->input->get('published', true); if($passedSichtbar !== null) { $sichtbar = $passedSichtbar; } $passedIsActive = $this->input->get('isActive', true); if ($passedIsActive !== null) { $isActive = filter_var( $passedIsActive, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); if ($isActive !== null) { $active = $isActive; } } $degreeProgramShortCode = $this->input->get('degreeProgramShortCode', true); if ($degreeProgramShortCode !== null && $degreeProgramShortCode !== '') { if (!ctype_digit((string)$degreeProgramShortCode)) { $this->terminateWithValidationErrors([ 'degreeProgramShortCode' => 'A valid degree program is required' ]); } $studiengang_kz = $degreeProgramShortCode; } $passedSemester = $this->input->get('semester', true); if ($passedSemester !== null && $passedSemester !== '') { if ( !ctype_digit((string)$passedSemester) || (int)$passedSemester < 1 || (int)$passedSemester > 8 ) { $this->terminateWithValidationErrors([ 'semester' => 'A valid semester is required' ]); } $semester = filter_var($passedSemester, FILTER_VALIDATE_INT); } $entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news"); $news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, false, $titel, true, $sichtbar, $page, $page_size, $sprache, true, $entitledDegreePrograms, $active); $news = $this->getDataOrTerminateWithError($news); $this->addMeta('row_count', $news["row_count"] ?? 0); $this->terminateWithSuccess($news["content"]); } /** * Returns one news entry with all available language versions for editing. * * @return void */ public function getNewsItem($newsId) { if (!is_numeric($newsId) || (int)$newsId < 1) { $this->terminateWithValidationErrors([ 'newsId' => 'A valid news ID is required' ]); } $newsId = (int)$newsId; $newsResult = $this->NewsModel->load($newsId); if (isError($newsResult)) { $this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB); } if (!hasData($newsResult)) { $this->terminateWithValidationErrors([ 'newsId' => 'The requested news entry does not exist' ]); } $news = current(getData($newsResult)); $entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news"); if (!in_array($news->studiengang_kz, $entitledDegreePrograms)) { $this->terminateWithError('You are not entitled to access this news entry', self::ERROR_TYPE_PERMISSION); } $translationsResult = $this->ContentspracheModel->loadWhere([ 'content_id' => $news->content_id ]); if (isError($translationsResult)) { $this->terminateWithError(getError($translationsResult), self::ERROR_TYPE_DB); } $languages = []; foreach ((array)getData($translationsResult) as $translation) { $languages[$translation->sprache] = true; } $translations = []; foreach (array_keys($languages) as $language) { $contentResult = $this->ContentModel->getContent( $news->content_id, $language, null, null, false ); if (isError($contentResult)) { $this->terminateWithError(getError($contentResult), self::ERROR_TYPE_DB); } $content = getData($contentResult); $document = new DOMDocument('1.0', 'UTF-8'); if ($content->content !== '' && $document->loadXML($content->content) === false) { $this->terminateWithError( 'The stored news content is invalid', self::ERROR_TYPE_GENERAL ); } $author = $document->getElementsByTagName('verfasser')->item(0); $title = $document->getElementsByTagName('betreff')->item(0); $text = $document->getElementsByTagName('text')->item(0); $translations[] = [ 'language' => $language, 'author' => $author ? $author->nodeValue : '', 'title' => $title ? $title->nodeValue : '', 'text' => $text ? str_replace('dms.php', APP_ROOT . 'cms/dms.php', $text->nodeValue) : '', 'isPublished' => (bool)$content->sichtbar ]; } $this->terminateWithSuccess([ 'newsId' => (int)$news->news_id, 'visibleFrom' => $news->datum, 'visibleTo' => $news->datum_bis, 'degreeProgramShortCode' => $news->studiengang_kz, 'semester' => $news->semester, 'translations' => $translations ]); } /** * Creates a news entry and all supplied translations. * * @return void */ public function storeNewsItem() { $this->load->library('form_validation'); $postData = $this->input->post(null, true); $this->form_validation->set_data(is_array($postData) ? $postData : []); $this->form_validation->set_rules('visibleFrom', 'Visible from', 'required|is_valid_date'); $this->form_validation->set_rules('visibleTo', 'Visible to', 'is_valid_date'); $this->form_validation->set_rules( 'degreeProgramShortCode', 'Degree program', 'required|is_natural' ); $this->form_validation->set_rules( 'semester', 'Semester', 'is_natural_no_zero|less_than_equal_to[8]' ); $errors = $this->form_validation->run() === false ? $this->form_validation->error_array() : []; $visibleFrom = $this->input->post('visibleFrom', true); $visibleTo = $this->input->post('visibleTo', true); $translations = $this->input->post('translations', true); if ( is_string($visibleFrom) && is_string($visibleTo) && $visibleTo !== '' && is_valid_date($visibleFrom) && is_valid_date($visibleTo) && new DateTime($visibleTo) < new DateTime($visibleFrom) ) { $errors['visibleTo'] = 'Visible to must not be before Visible from'; } if (!is_array($translations) || count($translations) === 0) { $errors['translations'] = 'At least one translation is required'; } else { foreach ($translations as $index => $translation) { if (!is_array($translation)) { $errors['translations.' . $index] = 'Each translation must be an object'; } } } if (count($errors) > 0) { $this->terminateWithValidationErrors($errors); } $this->_ensureUserIsEntitledForDegreeProgram($this->input->post('degreeProgramShortCode', true)); $degreeProgramShortCode = $this->input->post('degreeProgramShortCode', true); $semester = $this->input->post('semester', true); $semester = $semester === '' ? null : $semester; $visibleTo = $visibleTo === '' ? null : $visibleTo; $studiengang = $this->_getStudiengang($degreeProgramShortCode); $data = [ 'studiengang_kz' => $degreeProgramShortCode, 'semester' => $semester, 'datum' => $visibleFrom, 'datum_bis' => $visibleTo, 'translations' => [] ]; foreach ($translations as $index => $translation) { $data['translations'][$index] = [ 'sprache' => $translation['language'] ?? '', 'verfasser' => $translation['author'] ?? '', 'betreff' => $translation['title'] ?? '', 'text' => $translation['text'] ?? '', 'sichtbar' => isset($translation['isPublished']) ? (bool)$translation['isPublished'] : false ]; } $this->_validateLanguages($data['translations']); foreach ($data['translations'] as $index => $translation) { $data['translations'][$index]['content'] = $this->_buildNewsXml($translation); } $uid = getAuthUID(); $now = date('Y-m-d H:i:s'); if ($this->db->trans_begin() === false) { $this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB); } $contentResult = $this->ContentModel->insert([ 'template_kurzbz' => 'news', 'oe_kurzbz' => $studiengang->oe_kurzbz, 'aktiv' => true, 'menu_open' => false, 'insertamum' => $now, 'insertvon' => $uid ]); $contentId = (int)$this->_getInsertedIdOrRollback($contentResult); $newsResult = $this->NewsModel->insert([ 'uid' => $uid, 'studiengang_kz' => $data['studiengang_kz'], 'semester' => $data['semester'], 'datum' => $data['datum'], 'datum_bis' => $data['datum_bis'], 'content_id' => $contentId, 'insertamum' => $now, 'insertvon' => $uid, 'updateamum' => $now, 'updatevon' => $uid ]); $newsId = (int)$this->_getInsertedIdOrRollback($newsResult); foreach ($data['translations'] as $translation) { $translation['content'] = $this->formatContentURLsToRelative($translation['content']); $translationResult = $this->ContentspracheModel->insert([ 'sprache' => $translation['sprache'], 'content_id' => $contentId, 'version' => 1, 'sichtbar' => $translation['sichtbar'], 'content' => $translation['content'], 'titel' => $translation['betreff'], 'insertamum' => $now, 'insertvon' => $uid, 'updateamum' => $now, 'updatevon' => $uid ]); $this->_getInsertedIdOrRollback($translationResult); } $newsItemResult = $this->NewsModel->load($newsId); $newsItem = $this->getDataOrTerminateWithError($newsItemResult); $newsItem = $newsItem[0]; $this->_sendNewsItemTranslationRequiredMail( $newsItem ); if ($this->db->trans_status() === false) { $error = $this->db->error(); $this->db->trans_rollback(); $this->terminateWithError($error, self::ERROR_TYPE_DB); } if ($this->db->trans_commit() === false) { $error = $this->db->error(); $this->db->trans_rollback(); $this->terminateWithError($error, self::ERROR_TYPE_DB); } $this->terminateWithSuccess($newsId); } /** * Updates a news entry and its supplied translations. * * @return void */ public function updateNewsItem($newsId) { if (!is_numeric($newsId) || (int)$newsId < 1) { $this->terminateWithValidationErrors([ 'newsId' => 'A valid news ID is required' ]); } $newsId = (int)$newsId; $existingNewsResult = $this->NewsModel->load($newsId); if (isError($existingNewsResult)) { $this->terminateWithError(getError($existingNewsResult), self::ERROR_TYPE_DB); } if (!hasData($existingNewsResult)) { $this->terminateWithValidationErrors([ 'newsId' => 'The requested news entry does not exist' ]); } $existingNews = current(getData($existingNewsResult)); $this->_ensureUserIsEntitledForDegreeProgram($existingNews->studiengang_kz); $this->load->library('form_validation'); $postData = $this->input->post(null, true); $this->form_validation->set_data(is_array($postData) ? $postData : []); $this->form_validation->set_rules('visibleFrom', 'Visible from', 'required|is_valid_date'); $this->form_validation->set_rules('visibleTo', 'Visible to', 'is_valid_date'); $this->form_validation->set_rules( 'degreeProgramShortCode', 'Degree program', 'required|is_natural' ); $this->form_validation->set_rules( 'semester', 'Semester', 'is_natural_no_zero|less_than_equal_to[8]' ); $errors = $this->form_validation->run() === false ? $this->form_validation->error_array() : []; $visibleFrom = $this->input->post('visibleFrom', true); $visibleTo = $this->input->post('visibleTo', true); $translations = $this->input->post('translations', true); if ( is_string($visibleFrom) && is_string($visibleTo) && $visibleTo !== '' && is_valid_date($visibleFrom) && is_valid_date($visibleTo) && new DateTime($visibleTo) < new DateTime($visibleFrom) ) { $errors['visibleTo'] = 'Visible to must not be before Visible from'; } if (!is_array($translations) || count($translations) === 0) { $errors['translations'] = 'At least one translation is required'; } else { foreach ($translations as $index => $translation) { if (!is_array($translation)) { $errors['translations.' . $index] = 'Each translation must be an object'; } } } if (count($errors) > 0) { $this->terminateWithValidationErrors($errors); } $degreeProgramShortCode = $this->input->post('degreeProgramShortCode', true); $semester = $this->input->post('semester', true); $semester = $semester === '' ? null : $semester; $visibleTo = $visibleTo === '' ? null : $visibleTo; $studiengang = $this->_getStudiengang($degreeProgramShortCode); $data = [ 'studiengang_kz' => $degreeProgramShortCode, 'semester' => $semester, 'datum' => $visibleFrom, 'datum_bis' => $visibleTo, 'translations' => [] ]; foreach ($translations as $index => $translation) { $data['translations'][$index] = [ 'sprache' => $translation['language'] ?? '', 'verfasser' => $translation['author'] ?? '', 'betreff' => $translation['title'] ?? '', 'text' => $translation['text'] ?? '', 'sichtbar' => isset($translation['isPublished']) ? (bool)$translation['isPublished'] : false ]; } $this->_validateLanguages($data['translations']); foreach ($data['translations'] as $index => $translation) { $data['translations'][$index]['content'] = $this->_buildNewsXml($translation); } $uid = getAuthUID(); $now = date('Y-m-d H:i:s'); if ($this->db->trans_begin() === false) { $this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB); } $contentResult = $this->ContentModel->update($existingNews->content_id, [ 'oe_kurzbz' => $studiengang->oe_kurzbz, 'updateamum' => $now, 'updatevon' => $uid ]); if (isError($contentResult)) { $this->db->trans_rollback(); $this->terminateWithError(getError($contentResult), self::ERROR_TYPE_DB); } $newsResult = $this->NewsModel->update($newsId, [ 'studiengang_kz' => $data['studiengang_kz'], 'semester' => $data['semester'], 'datum' => $data['datum'], 'datum_bis' => $data['datum_bis'], 'updateamum' => $now, 'updatevon' => $uid ]); if (isError($newsResult)) { $this->db->trans_rollback(); $this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB); } $existingTranslationsResult = $this->ContentspracheModel->loadWhere([ 'content_id' => $existingNews->content_id ]); if (isError($existingTranslationsResult)) { $this->db->trans_rollback(); $this->terminateWithError(getError($existingTranslationsResult), self::ERROR_TYPE_DB); } $existingTranslations = []; foreach ((array)getData($existingTranslationsResult) as $existingTranslation) { $language = $existingTranslation->sprache; if ( !isset($existingTranslations[$language]) || (int)$existingTranslation->version > (int)$existingTranslations[$language]->version ) { $existingTranslations[$language] = $existingTranslation; } } $submittedLanguages = array_column($data['translations'], 'sprache'); $this->db->where('content_id', $existingNews->content_id); $this->db->where_not_in('sprache', $submittedLanguages); if ($this->db->delete('campus.tbl_contentsprache') === false) { $error = $this->db->error(); $this->db->trans_rollback(); $this->terminateWithError($error, self::ERROR_TYPE_DB); } foreach ($data['translations'] as $translation) { $translation['content'] = $this->formatContentURLsToRelative($translation['content']); $translationData = [ 'sichtbar' => $translation['sichtbar'], 'content' => $translation['content'], 'titel' => $translation['betreff'], 'updateamum' => $now, 'updatevon' => $uid ]; $existingTranslation = $existingTranslations[$translation['sprache']] ?? null; if ($existingTranslation) { $translationResult = $this->ContentspracheModel->update( $existingTranslation->contentsprache_id, $translationData ); } else { $translationResult = $this->ContentspracheModel->insert([ 'sprache' => $translation['sprache'], 'content_id' => $existingNews->content_id, 'version' => 1, 'insertamum' => $now, 'insertvon' => $uid ] + $translationData); } $this->_getInsertedIdOrRollback($translationResult); } if ($this->db->trans_status() === false) { $error = $this->db->error(); $this->db->trans_rollback(); $this->terminateWithError($error, self::ERROR_TYPE_DB); } if ($this->db->trans_commit() === false) { $error = $this->db->error(); $this->db->trans_rollback(); $this->terminateWithError($error, self::ERROR_TYPE_DB); } $this->terminateWithSuccess($newsId); } /** * Deletes one news entry and its linked content. * * @return void */ public function deleteNewsItem($newsId) { if (!is_numeric($newsId) || (int)$newsId < 1) { $this->terminateWithValidationErrors([ 'newsId' => 'A valid news ID is required' ]); } $newsId = (int)$newsId; $newsResult = $this->NewsModel->load($newsId); if (isError($newsResult)) { $this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB); } if (!hasData($newsResult)) { $this->terminateWithValidationErrors([ 'newsId' => 'The requested news entry does not exist' ]); } $news = current(getData($newsResult)); $this->_ensureUserIsEntitledForDegreeProgram($news->studiengang_kz); if ($this->db->trans_begin() === false) { $this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB); } $newsDeleteResult = $this->NewsModel->delete($newsId); if (isError($newsDeleteResult)) { $this->db->trans_rollback(); $this->terminateWithError(getError($newsDeleteResult), self::ERROR_TYPE_DB); } $this->db->where('content_id', $news->content_id); if ($this->db->delete('campus.tbl_contentsprache') === false) { $error = $this->db->error(); $this->db->trans_rollback(); $this->terminateWithError($error, self::ERROR_TYPE_DB); } $contentDeleteResult = $this->ContentModel->delete($news->content_id); if (isError($contentDeleteResult)) { $this->db->trans_rollback(); $this->terminateWithError(getError($contentDeleteResult), self::ERROR_TYPE_DB); } if ($this->db->trans_status() === false) { $error = $this->db->error(); $this->db->trans_rollback(); $this->terminateWithError($error, self::ERROR_TYPE_DB); } if ($this->db->trans_commit() === false) { $error = $this->db->error(); $this->db->trans_rollback(); $this->terminateWithError($error, self::ERROR_TYPE_DB); } $this->terminateWithSuccess($newsId); } //------------------------------------------------------------------------------------------------------------------ // Private methods /** * Loads the requested Studiengang or reports a validation error. * * @param integer $studiengangKz * @return stdClass */ private function _getStudiengang($studiengangKz) { $result = $this->StudiengangModel->load($studiengangKz); if (isError($result)) { $this->terminateWithError(getError($result), self::ERROR_TYPE_DB); } if (!hasData($result)) { $this->terminateWithValidationErrors([ 'studiengang_kz' => 'The requested Studiengang does not exist' ]); } $studiengaenge = getData($result); return current($studiengaenge); } /** * Ensures that all supplied languages exist in tbl_sprache. * * @param array $translations * @return void */ private function _validateLanguages($translations) { $languages = []; foreach ($translations as $translation) { $languages[] = $translation['sprache']; } $result = $this->SpracheModel->loadMultiple($languages); if (isError($result)) { $this->terminateWithError(getError($result), self::ERROR_TYPE_DB); } $existingLanguages = []; foreach ((array)getData($result) as $language) { $existingLanguages[$language->sprache] = true; } $errors = []; foreach ($translations as $index => $translation) { if (!isset($existingLanguages[$translation['sprache']])) { $errors['translations.' . $index . '.sprache'] = 'The requested language does not exist'; } } if (count($errors) > 0) { $this->terminateWithValidationErrors($errors); } } private function _ensureUserIsEntitledForDegreeProgram($degreeProgramShortCode) { $entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news:w"); if (!is_array($entitledDegreePrograms)) { $this->terminateWithError('Failed to retrieve entitled degree programs for the user', self::ERROR_TYPE_GENERAL); } if (!in_array($degreeProgramShortCode, $entitledDegreePrograms)) { $this->terminateWithError("User is not entitled for the specified degree program"); } } /** * Returns an inserted identifier or rolls the transaction back on error. * * @param stdClass $result * @return mixed */ private function _getInsertedIdOrRollback($result) { if (isError($result)) { $this->db->trans_rollback(); $this->terminateWithError(getError($result), self::ERROR_TYPE_DB); } return getData($result); } /** * Builds the XML format consumed by existing news readers. * * @param array $translation * @return string */ private function _buildNewsXml($translation) { $document = new DOMDocument('1.0', 'UTF-8'); $news = $document->createElement('news'); $document->appendChild($news); $this->_appendCdataElement($document, $news, 'verfasser', $translation['verfasser']); $this->_appendCdataElement($document, $news, 'betreff', $translation['betreff']); $this->_appendCdataElement($document, $news, 'text', $translation['text']); return $document->saveXML($news); } /** * Appends a CDATA element and safely preserves CDATA terminators. * * @param DOMDocument $document * @param DOMElement $parent * @param string $name * @param string $value * @return void */ private function _appendCdataElement($document, $parent, $name, $value) { $element = $document->createElement($name); $parts = explode(']]>', $value); $lastIndex = count($parts) - 1; foreach ($parts as $index => $part) { if ($index > 0) { $part = '>' . $part; } if ($index < $lastIndex) { $part .= ']]'; } $element->appendChild($document->createCDATASection($part)); } $parent->appendChild($element); } private function _sendNewsItemTranslationRequiredMail($newsItem) { $translatorsResult = $this->BenutzerfunktionModel->getBenutzerFunktionen("translate", $newsItem->studiengang_kz); $translators = $this->getDataOrTerminateWithError($translatorsResult); $newsItemContentResult = $this->ContentspracheModel->loadWhere([ 'content_id' => $newsItem->content_id, 'sprache' => 'German' ]); $newsItemContentEntry = getData($newsItemContentResult)[0] ?? null; if ($newsItemContentEntry) { $newsItem->sichtbar = $newsItemContentEntry->sichtbar ?? false; $newsItem->content = $newsItemContentEntry->content; } $content = getData( $this->cmslib->createNewsContent( $this->cmslib->createNewsWrapper($newsItem, true), false, "" ) ); foreach ($translators as $translator) { sendSanchoMail( "Sancho_Mail_News_Translation_Req", [ "newsItemUrl" => APP_ROOT . "cis.php/CisVue/Cms/newsAdministration?newsId=" . $newsItem->news_id, "newsItemAuthor" => $newsItem->verfasser, "newsItemSubject" => $newsItem->betreff, "newsItemContent" => $content ], $translator->uid . "@" . DOMAIN, "Übersetzung erforderlich / Translation required" ); } } private function formatContentURLsToRelative($content) { $content = preg_replace( '#https?://[^"\'>\s]*/(dms\.php(?:\?[^"\'>\s]*)?)#i', '$1', $content ); $content = preg_replace( '#(?:\.\./)+[^"\'>\s]*/(dms\.php(?:\?[^"\'>\s]*)?)#i', '$1', $content ); $content = preg_replace( '#/[^"\'>\s]*/(dms\.php(?:\?[^"\'>\s]*)?)#i', '$1', $content ); return $content; } }