Compare commits

..
Author SHA1 Message Date
Paolo e94eb33d81 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-10-05 14:55:32 +02:00
Harald Bamberger 4ba2869632 Merge branch 'master' into bugfix-83059_incorrect-ical-semester-parameters 2026-10-05 11:22:59 +02:00
Andreas Österreicher 293bb77179 Merge branch 'bug-44023/EingabemaskeEntwicklungsteamNamenOhneUid2' 2026-10-02 13:23:02 +02:00
Andreas Österreicher 3fe10d715d Fixed Saving of Entwicklungsteam 2026-10-02 13:14:41 +02:00
Andreas Österreicher 36064afbba Fixed Testing Projektarbeit Seeds 2026-10-01 15:33:05 +02:00
Harald Bamberger f3d031ab16 cismenu: add cis-main-menu class to main nav, add li elements to ul, style cis-main-menu ul and li to prevent interferec with cms content styles, add draggable false to cis menu entries to improve menu scrolling especially on mobile devices 2026-10-01 11:48:51 +02:00
Harald Bamberger 3d8c4fa4d0 Merge branch 'feature-77757/WidgetsBerechtigung' 2026-09-30 15:48:00 +02:00
Harald Bamberger 272014bd78 remove odd phrases 2026-09-30 15:38:43 +02:00
Harald Bamberger 76a8e00edf Merge branch 'master' into feature-77757/WidgetsBerechtigung 2026-09-30 15:23:08 +02:00
Paolo 5d6a6b9413 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-09-30 13:55:02 +02:00
Harald Bamberger c8c7d1566c Merge branch 'master' into bugfix-83059_incorrect-ical-semester-parameters 2026-09-11 11:43:23 +02:00
Paolo ae935326d1 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-09-10 12:30:03 +02:00
adisposkofh 9d4218bf38 implemented displayed semester/ical dates changes from my lv plan to other lv plan 2026-09-09 11:36:19 +02:00
adisposkofh 67f30c7765 fixed issue with wrong semester displayed and wrong ical dates used in my lv plan 2026-09-09 11:30:43 +02:00
Paolo dee1db8282 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-08-13 10:40:14 +02:00
Johann Hoffmann d40040efd8 added new fkey to tbl_widget for system.tbl_berechtigung; check for permission status during load & in widget picker; if widget was already picked/configured but not permitted anymore -> show a generalized "missing permission" screen via fallback component during widget init; 2026-07-21 18:14:26 +02:00
Paolo 10896909d4 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-07-09 10:48:04 +02:00
Paolo 8ae4a0ef3d Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-06-11 12:49:46 +02:00
Paolo 92465fd943 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-04-27 13:03:56 +02:00
Paolo 5df7358aff Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-03-24 10:51:09 +01:00
Paolo c70129cf37 Code cleaning OP#71539 2026-02-23 14:55:50 +01:00
Paolo 4b8f757a7c Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-02-23 12:26:37 +01:00
Alexei Karpenko e1bad61a27 api/frontend/Documents.php: removed addmeta debugging calls 2026-02-09 18:53:51 +01:00
Alexei Karpenko f4d8a396f5 document export bugfixes: getting content from db in Documents.php, correctly return data in Dokumente.php, passing temp folder in DocumentExportLib, path fix in DocumentLib, correct parameters in Archiv.php getByPersonId call, Archiv.js: check if array is returned, different controller for download (dokumente instead of akte) 2026-02-09 14:22:39 +01:00
Paolo 0b797cfdb1 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-02-09 12:35:12 +01:00
Alexei Karpenko 7909edb402 fixed bug in FHC_Controller outputFile method: corrected filename header name 2026-02-03 17:32:38 +01:00
Paolo 7a7cda3488 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2026-01-13 16:38:13 +01:00
Paolo 3f52b8c8d1 Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2025-10-21 15:04:48 +02:00
Paolo deaacc494d Fixed indentation 2025-10-01 11:05:41 +02:00
Paolo 21733d1e5e Merge branch 'master' into feature-61599/DocumentExportLib_erweitern 2025-10-01 09:39:40 +02:00
Paolo 28ef0b5892 Replaced global function uploadFile with the protected method FHC_Controller->uploadFile 2025-09-29 09:37:39 +02:00
Paolo 1657ae8291 - Removed global function uploadFile from application/helpers/hlp_common_helper.php
- Added new protected method uploadFile to application/core/FHC_Controller.php
- Replaced global function uploadFile with the protected method FHC_Controller->uploadFile
2025-09-29 09:32:21 +02:00
Paolo 517f39dc1f Removed unused parameter from the application/libraries/DmsLib.php constructor 2025-09-24 14:29:31 +02:00
Paolo b129bb659b - application/libraries/DocumentExportLib.php now makes use of the application/libraries/SignatureLib.php
- Fixex & improvements
2025-09-18 10:40:28 +02:00
Paolo d54ab06ad4 - Removed DocumentExportLib->showContent
- Adapted code to replace DocumentExportLib->showContent with DocumentExportLib->getContent + file download
- DocumentExportLib now makes use of the DocumentLib
2025-09-17 16:53:14 +02:00
Paolo 2dc341e048 Removed DMS_PATH from the file name in the DmsLib->getOutputFileInfo 2025-09-16 17:37:55 +02:00
Paolo 36f857428e - Fixed typos
- application/controllers/Cis/Pub.php bild method refactored
- Replaced AkteModel with AkteLib where possible
- application/core/FHC_Controller->outputFile added new features + improvements
- Added new public methods outputImageByContent and outputImageByFile to application/core/FHC_Controller
- Added new private method _outputImage to application/core/FHC_Controller
- AkteLib fixes
2025-09-16 17:12:59 +02:00
Paolo 909eb0bf57 Fixed AkteLib->_remove 2025-09-15 17:42:40 +02:00
Paolo 949fdfa5ca - DmsLib->download removed
- DmsLib->upload replaced by uploadFile
- Removed who parameter from DmsLib constructor
- Added insert by into DmsLib add and addLastVersion
- Removed commented code
- Replaced self with $this in the Anrechnung classes
- Added removeByPersonIdAndDmsId to AkteLib
2025-09-15 17:16:48 +02:00
Paolo 6edc8a127f Fixed application/libraries/DocumentExportLib.php to generate docs with DocsboxLib 2025-09-09 15:55:56 +02:00
Paolo eaacbcf7cc - Bugfix in application/controllers/Cis/Documents.php
- application/libraries/AkteLib.php->_get fixes
- b/application/libraries/DmsLib.php better handling when no DMS is found
2025-09-09 15:48:53 +02:00
Paolo 88cdd43b3e Fixed AkteLib 2025-09-09 14:28:36 +02:00
Paolo 2ee0c95467 - DmsLib:
- Removed deprecated methods from the DMSLib, these were the "problematic" types of methods:
		- Methods that would perform an upload/download of the file (should be performed by a controller)
		- Methods that were returning a base64 representation of the file (what if the file is a 1Gb?)
		- Methods that were performing operation on the akte (should be done in the AkteLib)
	- The first two types have been removed and added new utility method to read a file (wrapper of the DMSFSModel)
	- The last type of methods has been removed and same functionalities already exists in the AkteLib
	- Minor fixes
- AkteLib:
	- Added new methods to get an akte with relative content (even from the DMS) with different parameters (akte_id, person_id, dokument_kurzbz, signiert, stud_selfservice)
- AkteModel:
	- Removed the getArchiv method
- Replaced the usage of the AkteModel with the AkteLib as much as possible. AkteModel is used mainly to quickly check if an akte exists or not, otherwise the AkteLib is used for read/write operations
- Replaced the DmsLib usage with the AkteLib usage where the old deprecated DmsLib methods, that were working with the akte too, were used
2025-09-09 13:28:56 +02:00
115 changed files with 1284 additions and 14711 deletions
-21
View File
@@ -1,21 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
// Preferred organisational unit for a new content.
// If the user is entitled for this unit, it wins.
// If not, the first entitled unit wins.
$config['default_oe_kurzbz'] = 'etw';
// Lifetime of an edit lock in hours, counted from tbl_contentlog.start. Must be > 0.
$config['lock_ttl_hours'] = 24;
// Switches the click statistics off completely. With false the admin tab stays hidden and
// the endpoint refuses every request, whatever the rights of the caller are.
$config['clickstats_enabled'] = true;
// Default period of the click statistics in months, when the caller sends none.
$config['clickstats_months'] = 12;
// Length of the ranking in the click statistics tab.
$config['clickstats_limit'] = 100;
-4
View File
@@ -75,10 +75,6 @@ $route['Abgabetool/Student'] = 'Cis/Abgabetool/Student';
$route['Abgabetool/Student/(:any)'] = 'Cis/Abgabetool/Student/$1';
$route['Abgabetool/Deadlines'] = 'Cis/Abgabetool/Deadlines';
// CMS Admin App Routes
$route['cms'] = 'cms/index';
$route['cms/.*'] = 'cms/index';
// Studierendenverwaltung App Routes
$route['studvw'] = 'studentenverwaltung/index';
$route['studvw/.*'] = 'studentenverwaltung/index';
+10 -5
View File
@@ -20,8 +20,12 @@ class Documents extends Auth_Controller
'download' => [self::PERM_LOGGED]
]);
// Load models
$this->load->model('crm/Prestudentstatus_model', 'PrestudentstatusModel');
// Load libraries
$this->load->library('AkteLib');
$this->loadPhrases([
'global',
'tools'
@@ -112,10 +116,12 @@ class Documents extends Auth_Controller
$selfservice = null;
if (!defined('CIS_DOKUMENTE_SELFSERVICE') || CIS_DOKUMENTE_SELFSERVICE) {
$this->load->model('crm/Akte_model', 'AkteModel');
$selfservice = [];
foreach ($person_ids as $person_id) {
$result = $this->AkteModel->getArchiv($person_id, null, true);
$result = $this->aktelib->getByPersonId(
$person_id,
true
);
if (isError($result))
return $this->load->view('errors/html/error_db.php', [
'heading' => 'Database Error',
@@ -147,14 +153,13 @@ class Documents extends Auth_Controller
if (!is_numeric($akte_id))
return show_404();
$this->load->model('crm/Akte_model', 'AkteModel');
$result = $this->AkteModel->load($akte_id);
$result = $this->aktelib->getByAkteId($akte_id);
if (isError($result))
return show_error(getError($result));
$akte = getData($result);
if (!$akte)
return show_404();
$akte = current($akte);
$akte = reset($akte);
$admin_access = false;
if ($uid !== null && $this->permissionlib->isBerechtigt('admin')) {
+2 -1
View File
@@ -64,7 +64,7 @@ class ProfilUpdate extends Auth_Controller
{
// Get file to be downloaded from DMS
$newFilename = $this->uid . "/document_" . $dms_id;
$download = $this->dmslib->download($dms_id);
$download = $this->dmslib->getOutputFileInfo($dms_id);
if (isError($download))
return $download;
@@ -84,3 +84,4 @@ class ProfilUpdate extends Auth_Controller
}
}
}
+48 -82
View File
@@ -29,10 +29,14 @@ class Pub extends Auth_Controller
*/
public function bild($source, $id)
{
$this->load->model('person/Person_model', 'PersonModel');
$person_id_user = '';
$person_id_user = getAuthPersonId();
$serverzugriff = false;
// Default Bild (Dummy Profilbild)
$outputFileName = FHCPATH . 'skin/images/profilbild_dummy.jpg';
$outputFileContent = '';
$mimetype = 'application/jpeg';
$this->load->model('person/Person_model', 'PersonModel');
// Wenn das Bild direkt aufgerufen wird, ist eine Authentifizierung erforderlich
// Wenn es vom Server selbst aufgerufen wird, ist keine Auth. notwendig
@@ -45,31 +49,27 @@ class Pub extends Auth_Controller
'zugangscode' => $_SESSION['incoming/user']
]);
if (hasData($result))
$person_id_user = current(getData($result))->person_id;
$person_id_user = getData($result)[0]->person_id;
} elseif (isset($_SESSION['prestudent/user'])) { // Von Prestudententool
$result = $this->PersonModel->loadWhere([
'zugangscode' => $_SESSION['prestudent/user']
]);
if (hasData($result))
$person_id_user = current(getData($result))->person_id;
$person_id_user = getData($result)[0]->person_id;
} elseif (isset($_SESSION['bewerbung/personId'])) { // Von Bewerbungstool
$person_id_user = $_SESSION['bewerbung/personId'];
} else {
$person_id_user = getAuthPersonId();
}
} else {
$serverzugriff = true;
}
// Default Bild (Dummy Profilbild)
$cTmpHEX = base64_encode(file_get_contents(FHCPATH . 'skin/images/profilbild_dummy.jpg'));
if ($source == 'person' && $id) {
// If the picture is from the person table
if ($source == 'person' && is_numeric($id)) {
$foto_gesperrt = false;
// Person laden und Fotosperre überprüfen
$result = $this->PersonModel->load($id);
if (hasData($result)) {
$person = current(getData($result));
$person = getData($result)[0];
if ($person->foto_sperre) {
// Wenn der User selbst darauf zugreift darf er das Bild sehen
$foto_gesperrt = ($person_id_user != $id);
@@ -77,91 +77,57 @@ class Pub extends Auth_Controller
$foto_gesperrt = true;
}
if ($person->foto && !$foto_gesperrt) {
$cTmpHEX = base64_decode($person->foto);
if (!isEmptyString($person->foto) && !$foto_gesperrt) {
$outputFileContent = base64_decode($person->foto);
}
}
}
if($source == 'akte' && $id != '')
// If the picture is from the akte/dms
if($source == 'akte' && is_numeric($id))
{
$this->load->model('crm/Akte_model', 'AkteModel');
$mimetype = '';
$this->load->library('AkteLib');
$this->AkteModel->addJoin('public.tbl_person', 'person_id');
$result = $this->AkteModel->loadWhere([
'person_id' => $id,
'dokument_kurzbz' => 'Lichtbil'
]);
$akteResult = $this->aktelib->getByAkteId($id, 'Lichtbil');
if (hasData($result)) {
if (hasData($akteResult)) {
$foto_gesperrt = false;
$akte = getData($akteResult)[0];
$akte = current(getData($result));
if ($akte->foto_sperre) {
// Wenn der User selbst darauf zugreift darf er das Bild sehen
$foto_gesperrt = ($person_id_user != $id);
} elseif (!$person_id_user && !$serverzugriff) {
$foto_gesperrt = true;
}
// Wenn das Foto nicht im Inhalt steht wird aus aus dem DMS geladen
if (!$akte->inhalt && $akte->dms_id) {
$this->load->model('content/Dms_model', 'DmsModel');
$this->load->model('content/DmsVersion_model', 'DmsVersionModel');
$this->DmsModel->addJoin('campus.tbl_dms_version', 'dms_id');
$this->DmsModel->addOrder('version', 'DESC');
$this->DmsModel->addLimit(1);
$result = $this->DmsModel->load($akte->dms_id);
if (!hasData($result))
die('Kein Dokument vorhanden');
$dms = current(getData($result));
$filename = DMS_PATH . $dms->filename;
$this->DmsVersionModel->update([
'dms_id' => $dms->dms_id,
'version' => $dms->version
], [
'letzterzugriff' => date('c')
]);
if (file_exists($filename)) {
$handle = fopen($filename, "r");
if ($handle) {
while (!feof($handle)) {
$akte->inhalt .= fread($handle, 8192);
}
fclose($handle);
} else {
echo 'Fehler: Datei konnte nicht geoeffnet werden';
}
} else {
echo 'Die Datei existiert nicht';
$personResult = $this->PersonModel->load($akte->person_id);
if (hasData($personResult)) {
$person = getData($personResult)[0];
if ($person->foto_sperre) {
$foto_gesperrt = ($person_id_user != $id);
} elseif (!$person_id_user && !$serverzugriff) {
$foto_gesperrt = true;
}
}
if ($akte->inhalt && !$foto_gesperrt) {
$cTmpHEX = $akte->inhalt;
// Wenn das Foto nicht im Inhalt steht wird aus aus dem DMS geladen
if (!isEmptyString($akte->inhalt)) {
$outputFileContent = base64_decode($akte->inhalt);
$mimetype = $akte->mimetype;
}
}
}
// die bilder werden, sofern es funktioniert, in jpg umgewandelt da es sonst zu fehlern beim erstellen
// von pdfs kommen kann.
$im = @imagecreatefromstring(base64_decode($cTmpHEX));
if ($im) {
@ob_clean();
header("Content-type: image/jpeg");
exit(imagejpeg($im));
} else {
// bei manchen Bildern funktioniert die konvertierung nicht
// diese werden dann einfach so angezeigt.
@ob_clean();
header("Content-type: image/gif");
exit($cTmpHEX);
// If file content is provided
if (!isEmptyString($outputFileContent))
{
// If a mimetype is still not found
if (isEmptyString($mimetype))
{
$fo = finfo_open();
$mimetype = finfo_buffer($fo, $outputFileContent, FILEINFO_MIME_TYPE);
}
$this->outputImageByContent($mimetype, $outputFileContent);
}
else // otherwise use the file
{
$this->outputImageByFile($mimetype, $outputFileName);
}
}
}
+7 -10
View File
@@ -44,22 +44,19 @@ class Cms extends Auth_Controller
*
* @return void
*/
public function content($content_id = null, $version = null, $sprache = null, $sichtbar = true)
public function content($content_id, $version = null, $sprache = null, $sichtbar = true)
{
if (!is_numeric($content_id))
show_404();
// return early if the content_id for the content is missing
if (!isset($content_id))
$this->terminateWithError("content_id is missing");
$content = $this->ContentModel->load($content_id);
if (isError($content))
{
show_error('The content could not be loaded');
}
$this->terminateWithError(getError($content));
$content = getData($content);
if (NULL === $content) {
show_404();
}
if (NULL === $content)
$this->terminateWithError("Content not found");
$content = current($content);
-23
View File
@@ -1,23 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Cms extends Auth_Controller
{
public function __construct()
{
parent::__construct(['index' => 'basis/cms:r']);
$this->load->library('PermissionLib');
$this->config->load('cms');
$this->loadPhrases(['global', 'ui', 'cms']);
}
public function index()
{
// decide whether the click statistics tab is shown at all.
$this->load->view('Cms.php', [
'clickstats' => $this->config->item('clickstats_enabled')
&& $this->permissionlib->isBerechtigt('admin')
]);
}
}
+18 -6
View File
@@ -143,7 +143,7 @@ class Documents extends Auth_Controller
$result = $this->VorlagestudiengangModel->getCurrent($xsl, $xsl_oe_kurzbz, $version);
if (isError($result))
return show_error(getError($result));
show_error(getError($result));
if (!hasData($result))
return show_404();
@@ -226,13 +226,13 @@ class Documents extends Auth_Controller
$result = $this->VorlageModel->load($xsl);
if (isError($result))
return show_error(getError($result));
show_error(getError($result));
if (!hasData($result))
show_404();
$vorlage = current(getData($result));
if ($sign_user && !$vorlage->signierbar)
return show_error($this->p->t("stv", "grades_error_sign"));
show_error($this->p->t("stv", "grades_error_sign"));
// Filename
@@ -284,11 +284,23 @@ class Documents extends Auth_Controller
// XML Data
$result = $this->documentexportlib->getDataURL($xml, $params);
if (isError($result))
return show_error(getError($result));
show_error(getError($result));
$data = getData($result);
// Output
$this->documentexportlib->showContent($filename, $vorlage, $data, $xsl_oe_kurzbz, $version, $outputformat, $sign_user);
// Get the content
$contentResult = $this->documentexportlib->getContent($filename, $vorlage, $data, $xsl_oe_kurzbz, $version, $outputformat, $sign_user);
// If an error occurred
if (isError($contentResult)) show_error(getError($contentResult));
$fileObj = new stdClass();
$fileObj->file_content = getData($contentResult);
$fileObj->name = $filename;
$fileObj->mimetype = isEmptyString($vorlage->mimetype) ? 'application/pdf' : $vorlage->mimetype;
$fileObj->disposition = 'attachment';
// Download
$this->outputFile($fileObj);
}
}
+6 -340
View File
@@ -25,10 +25,6 @@ if (! defined('BASEPATH')) exit('No direct script access allowed');
*/
class Cms extends FHCAPI_Controller
{
// Upper bound for the tiefe of oe-personen and dms-liste. A hierarchy this deep does
// not occur, and the cap keeps one marker from walking a whole tree.
const MAX_TIEFE = 10;
/**
* Object initialization
@@ -43,11 +39,6 @@ class Cms extends FHCAPI_Controller
'news' => self::PERM_LOGGED,
'getNewsRowCount' => self::PERM_LOGGED,
'getNews' => self::PERM_LOGGED,
'getOePersonen' => self::PERM_LOGGED,
'getDmsKategorie' => self::PERM_LOGGED,
'getContentChilds' => self::PERM_LOGGED,
'getPerson' => self::PERM_LOGGED,
'getDmsDokumente' => self::PERM_LOGGED,
]);
@@ -64,7 +55,11 @@ class Cms extends FHCAPI_Controller
]);
}
//------------------------------------------------------------------------------------------------------------------
// Private methods
//------------------------------------------------------------------------------------------------------------------
// Public methods
@@ -85,10 +80,7 @@ class Cms extends FHCAPI_Controller
$sprache = $this->input->get("sprache",TRUE);
$sichtbar = $this->input->get("sichtbar",TRUE);
// cms admin preview renders a content without counting a view
$preview = $this->input->get("preview",TRUE);
$content = $this->cmslib->getContent($content_id, $version, $sprache, $sichtbar, !$preview);
$content = $this->cmslib->getContent($content_id, $version, $sprache, $sichtbar);
$content = $this->getDataOrTerminateWithError($content);
$this->terminateWithSuccess($content);
@@ -211,331 +203,5 @@ class Cms extends FHCAPI_Controller
}
/**
* Lists the people assigned to an organisation unit.
* Used by the CMS content component "oe-personen".
*/
public function getOePersonen()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('oe_kurzbz', 'Organisationseinheit', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel');
$this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel');
$oe_kurzbz = $this->input->get('oe_kurzbz', TRUE);
$mitFoto = $this->input->get('foto', TRUE) ? true : false;
// tiefe 0 asks about the one unit. Deeper takes the sub units with it.
$oeResult = $this->OrganisationseinheitModel->getMitUntereinheiten(
$oe_kurzbz, $this->_tiefe()
);
$oeRows = $this->getDataOrTerminateWithError($oeResult) ?: array();
$uids = array();
$funktionen = array();
foreach ($oeRows as $oe)
{
// 'oezuordnung' is the function that assigns an employee to an organisation
// unit. The model already filters datum_von and datum_bis.
$rows = $this->BenutzerfunktionModel->getBenutzerFunktionenDetailed(
'oezuordnung', $oe->oe_kurzbz
);
$rows = $this->getDataOrTerminateWithError($rows) ?: array();
foreach ($rows as $row)
{
// The model returns the aktiv flag but does not filter on it.
if (!$row->aktiv)
continue;
// One person can hold the assignment in several units of the walk. The
// first one wins, so the list keeps one block per person.
if (isset($funktionen[$row->uid]))
continue;
$uids[] = $row->uid;
// tbl_benutzerfunktion.bezeichnung is the free label of one assignment,
// for example "Bibliothekarin". tbl_funktion.beschreibung is the generic
// name.
$funktionen[$row->uid] = $row->bezeichnung ?: $row->beschreibung;
}
}
$this->terminateWithSuccess($this->_cmsPersonen($uids, $funktionen, $mitFoto));
}
/**
* Contact data of one person.
* Used by the CMS content component "person-block".
*/
public function getPerson()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('uid', 'UID', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$uid = $this->input->get('uid', TRUE);
$mitFoto = $this->input->get('foto', TRUE) ? true : false;
// The function label comes from the marker, not from the database. The component
// passes it through as a property.
$personen = $this->_cmsPersonen(array($uid), array(), $mitFoto);
$this->terminateWithSuccess($personen ? current($personen) : null);
}
/**
* Lists the published documents of one DMS category.
* Used by the CMS content component "dms-liste".
*/
public function getDmsKategorie()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('kategorie_kurzbz', 'DMS-Kategorie', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$kategorie_kurzbz = $this->input->get('kategorie_kurzbz', TRUE);
$erlaubt = $this->_dmsKategorieErlaubt($kategorie_kurzbz);
if ($erlaubt === null)
$this->terminateWithError('DMS category not found', self::ERROR_TYPE_404);
// a missing entitlement answers with success and an empty list, not with
// an error. The component sits inside a public content page. An error would raise a
// toast for every reader who is simply not in the group.
$dokumente = array();
if ($erlaubt)
{
// tiefe 0 asks about the one category. Deeper takes the sub categories with it.
$katResult = $this->DmsModel->getMitUnterkategorien(
$kategorie_kurzbz, $this->_tiefe()
);
$kategorien = $this->getDataOrTerminateWithError($katResult) ?: array();
foreach ($kategorien as $kat)
{
// Every category carries its own group rule, so a sub category can be
// closed while the parent is open. Ask for each one instead of letting the
// entitlement of the root stand for the whole walk. The root is already
// answered above.
if ($kat->kategorie_kurzbz !== $kategorie_kurzbz
&& $this->_dmsKategorieErlaubt($kat->kategorie_kurzbz) !== true)
continue;
$rows = $this->DmsModel->getKategorieDokumente($kat->kategorie_kurzbz);
$rows = $this->getDataOrTerminateWithError($rows) ?: array();
foreach ($rows as $row)
$dokumente[] = $row;
}
}
$this->terminateWithSuccess(array(
'zugriff' => $erlaubt,
'dokumente' => $dokumente
));
}
/**
* Lists named documents, in the order the caller asked for them.
* Used by the CMS content component "dms-dokumente".
*/
public function getDmsDokumente()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('dms_ids', 'DMS-IDs', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('content/Dms_model', 'DmsModel');
$ids = array();
foreach (explode(',', $this->input->get('dms_ids', TRUE)) as $teil)
{
$teil = trim($teil);
if (is_numeric($teil))
$ids[] = (int) $teil;
}
// An unbounded list would let one marker fetch the whole DMS.
$ids = array_slice(array_unique($ids), 0, 50);
$dokumente = $this->DmsModel->getDokumenteByIds($ids);
$dokumente = $this->getDataOrTerminateWithError($dokumente) ?: array();
// A hand picked list holds few categories, so deciding per category is cheap and
// reuses the rule of getDmsKategorie().
$erlaubte = array();
$nachId = array();
foreach ($dokumente as $dokument)
{
if (!array_key_exists($dokument->kategorie_kurzbz, $erlaubte))
$erlaubte[$dokument->kategorie_kurzbz] = $this->_dmsKategorieErlaubt($dokument->kategorie_kurzbz);
if ($erlaubte[$dokument->kategorie_kurzbz] === true)
$nachId[$dokument->dms_id] = $dokument;
}
// The editor chose the order. Keep it.
$sortiert = array();
foreach ($ids as $id)
{
if (isset($nachId[$id]))
$sortiert[] = $nachId[$id];
}
$this->terminateWithSuccess($sortiert);
}
/**
* Lists the child pages of a content, as a reader may see them.
* Used by the CMS content component "contentchild-menu".
*/
public function getContentChilds()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural_no_zero');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('content/Contentchild_model', 'ContentchildModel');
$content_id = $this->input->get('content_id', TRUE);
$sprache = $this->input->get('sprache', TRUE);
if (!$sprache)
$sprache = getUserLanguage();
$childs = $this->ContentchildModel->getChildsForReader($content_id, $sprache, getAuthUID());
$this->terminateWithSuccess($this->getDataOrTerminateWithError($childs) ?: array());
}
//------------------------------------------------------------------------------------------------------------------
// Private methods
/**
* Builds the shape both person content components render.
*
* The assembly follows Cis/Cms/News/Xml/Address/Detailed.php, so a person reads the
* same in the news extras and on a content page.
*
* @param array $uids in the order the result should keep
* @param array $funktionen uid => function label, may be empty
* @param boolean $mitFoto
* @return array
*/
private function _cmsPersonen($uids, $funktionen, $mitFoto)
{
if (empty($uids))
return array();
$this->load->model('person/Person_model', 'PersonModel');
$kontakt = $this->PersonModel->getCmsKontakt($uids, $mitFoto);
$kontakt = $this->getDataOrTerminateWithError($kontakt) ?: array();
$nachUid = array();
foreach ($kontakt as $row)
$nachUid[$row->uid] = $row;
$personen = array();
// Loops over $uids, not over the result, so the caller keeps its order.
foreach ($uids as $uid)
{
if (!isset($nachUid[$uid]))
continue;
$row = $nachUid[$uid];
$name = trim(implode(' ', array_filter(array(
$row->titelpre, $row->vorname, $row->nachname, $row->titelpost
))));
$telefon = '';
if ($row->telefonklappe)
$telefon = trim(($row->kontakt ?: '') . ' - ' . $row->telefonklappe);
$email = '';
if (defined('DOMAIN'))
$email = ($row->alias ?: $row->uid) . '@' . DOMAIN;
$personen[] = array(
'uid' => $row->uid,
'name' => $name,
'funktion' => isset($funktionen[$uid]) ? $funktionen[$uid] : '',
'telefon' => $telefon,
'email' => $email,
'ort' => $row->planbezeichnung,
'foto' => $row->foto
);
}
return $personen;
}
/**
* Decides whether the current user may see a DMS category.
*
* Same rule as cms/dms.php: an unlocked category is open to everyone. A locked one
* needs a granted group, or the category permission together with basis/dms.
*
* @param string $kategorie_kurzbz
* @return boolean|null null when the category does not exist
*/
/**
* Reads the tiefe parameter of a content component. 0 keeps the component to the unit
* or category it names, which is what a marker without the attribute means.
* Capped, because the depth drives a recursive walk and one query per level found.
*
* @return int
*/
private function _tiefe()
{
$tiefe = (int) $this->input->get('tiefe', TRUE);
if ($tiefe < 0)
return 0;
return min($tiefe, self::MAX_TIEFE);
}
private function _dmsKategorieErlaubt($kategorie_kurzbz)
{
$this->load->library('PermissionLib');
$this->load->model('content/Dms_model', 'DmsModel');
$zugriff = $this->DmsModel->getKategorieZugriff($kategorie_kurzbz, getAuthUID());
$zugriff = $this->getDataOrTerminateWithError($zugriff);
if (empty($zugriff))
return null;
$kategorie = current($zugriff);
if (!$kategorie->gesperrt || $kategorie->in_gruppe)
return true;
$erlaubt = $this->permissionlib->isBerechtigt('basis/dms');
if ($erlaubt && $kategorie->berechtigung_kurzbz != '')
$erlaubt = $this->permissionlib->isBerechtigt($kategorie->berechtigung_kurzbz);
return $erlaubt;
}
}
@@ -1,760 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class CmsAdmin extends FHCAPI_Controller
{
const MAX_TREE_DEPTH = 50;
public function __construct()
{
parent::__construct([
'getTree' => ['basis/cms:r'],
'getSubtree' => ['basis/cms:r'],
'getContent' => ['basis/cms:r'],
'getContentsprache' => ['basis/cms:r'],
'getTemplates' => ['basis/cms:r'],
'getOrganisationseinheiten' => ['basis/cms:r'],
'getSprachen' => ['basis/cms:r'],
'getDmsKategorien' => ['basis/cms:r'],
'getMitarbeiter' => ['basis/cms:r'],
'getDmsKategorieDokumente' => ['basis/cms:r'],
'getUsage' => ['basis/cms:r'],
'getTreeMeta' => ['basis/cms:r'],
'getAncestors' => ['basis/cms:r'],
'getClickCount' => ['basis/cms:r'],
'getClickRanking' => ['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);
}
// One subtree in the shape of a getTree root node. Lets the client replace a branch
// instead of rebuilding the whole tree after a change.
// The visited map starts empty here, so a node with several parents can show up twice
// until the next full reload. Rebuilding the whole tree on every change costs more.
public function getSubtree()
{
$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 = (int) $this->input->get('content_id', TRUE);
$entitledResult = $this->cmsadminlib->getEntitledOe();
$entitledOe = isError($entitledResult) ? [] : getData($entitledResult);
$visited = [];
$node = $this->buildTreeNode($content_id, 0, $visited);
// Deleted meanwhile. The client drops the branch.
if ($node === null)
$this->terminateWithSuccess(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);
$this->terminateWithSuccess($this->pruneUnentitled($node));
}
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($this->p->t('cms', 'keineBerechtigungFuerDiesenEintrag'));
$this->ContentModel->resetQuery();
$contentResult = $this->ContentModel->load($content_id);
$contentData = $this->getDataOrTerminateWithError($contentResult);
if (empty($contentData))
$this->terminateWithError($this->p->t('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;
// Tells the CIS4 preview whether this template renders there at all.
$cis4Result = $this->TemplateModel->hasCis4Stylesheet($content->template_kurzbz);
$result->has_cis4_stylesheet = !isError($cis4Result) && getData($cis4Result);
$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($this->p->t('cms', 'versionNichtGefunden'));
$data = getData($result);
$data->sichtbar = ($data->sichtbar === 't' || $data->sichtbar === true);
$this->terminateWithSuccess($data);
}
// The tree lists a search hit out of its place, so a generic title carries no context.
// This answers where each one sits. One walk for the whole list, not one per node.
const MAX_ANCESTOR_TIEFE = 12;
/**
* Ancestors of the given contents, keyed by content_id and ordered from the top down.
*/
public function getAncestors()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('content_ids', 'Content IDs', 'required');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$ids = [];
foreach (explode(',', $this->input->get('content_ids', TRUE)) as $teil)
{
$teil = trim($teil);
if (is_numeric($teil))
$ids[] = (int) $teil;
}
$ids = array_values(array_unique($ids));
if (empty($ids))
$this->terminateWithSuccess(new stdClass());
$result = $this->ContentchildModel->getAncestors(
$ids, self::MAX_ANCESTOR_TIEFE, DEFAULT_LANGUAGE
);
$rows = $this->getDataOrTerminateWithError($result);
// Same rule as the tree: an entry of an organisational unit the editor is not
// entitled for stays readable but cannot be opened.
$entitledResult = $this->cmsadminlib->getEntitledOe();
$entitledOe = isError($entitledResult) ? [] : getData($entitledResult) ?? [];
// The query orders by the distance, farthest first, so appending keeps the path
// reading from the top down.
$pfade = [];
foreach ((array) $rows as $row)
{
$start = (int) $row->start_id;
if (!isset($pfade[$start]))
$pfade[$start] = [];
$pfade[$start][] = [
'content_id' => (int) $row->content_id,
'titel' => $row->titel,
'tiefe' => (int) $row->tiefe,
'entitled' => in_array($row->oe_kurzbz, $entitledOe)
];
}
$this->terminateWithSuccess($pfade ?: new stdClass());
}
/**
* Per content metadata for the tree filter, keyed by content_id.
* One query for the whole tree, so the client filters and sorts without a round trip.
*/
public function getTreeMeta()
{
$result = $this->ContentModel->getTreeMeta(getAuthUID(), DEFAULT_LANGUAGE);
$rows = $this->getDataOrTerminateWithError($result);
$meta = [];
foreach ((array) $rows as $row)
{
$meta[(int) $row->content_id] = [
'childcount' => (int) $row->childcount,
'insertamum' => $row->insertamum,
'updateamum' => $row->updateamum,
'mine' => ($row->mine === 't' || $row->mine === true),
'contentlength' => (int) $row->contentlength
];
}
$this->terminateWithSuccess($meta);
}
/**
* Two gates guard every click statistics request: the config flag switches the feature
* off for everybody, the admin right decides who may read the numbers.
*/
private function checkClickstatsAccess()
{
$this->config->load('cms');
if (!$this->config->item('clickstats_enabled'))
$this->terminateWithError($this->p->t('cms', 'klickstatistikDeaktiviert'));
if (!$this->permissionlib->isBerechtigt('admin'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigung'));
}
/**
* Period of a click statistics request. 0 counts the whole log.
* Call it after checkClickstatsAccess, which loads the config.
* @return array months and the start date, null for the whole log
*/
private function clickstatsPeriod()
{
$months = $this->input->get('months');
if ($months === null || $months === '')
$months = $this->config->item('clickstats_months');
$months = (int) $months;
$since = ($months > 0)
? date('Y-m-d H:i:s', strtotime('-' . $months . ' months'))
: null;
return [$months, $since];
}
/**
* Views of one content. One counting query, so the tab loads it right away.
* Empty if LOG_CONTENT is off.
*/
public function getClickCount()
{
$this->checkClickstatsAccess();
$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('months', 'Monate', 'is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('system/Webservicelog_model', 'WebservicelogModel');
list($months, $since) = $this->clickstatsPeriod();
$result = $this->WebservicelogModel->getContentClickCount(
(int) $this->input->get('content_id', TRUE), $since
);
$this->terminateWithSuccess([
'since' => $since,
'months' => $months,
'own' => $this->getDataOrTerminateWithError($result)
]);
}
/**
* The ranking of the most viewed contents. It groups the whole log and resolves a
* title per row, which takes seconds, so the tab fetches it only on request.
* Empty if LOG_CONTENT is off.
*/
public function getClickRanking()
{
$this->checkClickstatsAccess();
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('months', 'Monate', 'is_natural');
if ($this->form_validation->run() == FALSE)
$this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('system/Webservicelog_model', 'WebservicelogModel');
list($months, $since) = $this->clickstatsPeriod();
$sprache = $this->input->get('sprache', TRUE);
if (empty($sprache))
$sprache = DEFAULT_LANGUAGE;
$result = $this->WebservicelogModel->getContentClickCounts(
$since, $sprache, (int) $this->config->item('clickstats_limit')
);
$rows = $this->getDataOrTerminateWithError($result);
$ranked = [];
foreach ((array) $rows as $row)
{
$ranked[] = [
'content_id' => (int) $row->content_id,
'titel' => $row->titel,
'hits' => (int) $row->hits
];
}
$this->terminateWithSuccess([
'since' => $since,
'months' => $months,
'ranked' => $ranked
]);
}
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(['content' => true]);
$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($this->p->t('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($this->p->t('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($this->p->t('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($this->p->t('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($this->p->t('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($this->p->t('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;
}
/**
* Lists the DMS categories for the contentcomponent insert dialog.
*/
public function getDmsKategorien()
{
$this->load->model('content/Dms_model', 'DmsModel');
$result = $this->DmsModel->getKategorien();
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
/**
* Lists active staff for the contentcomponent insert dialog.
* Names only. See Mitarbeiter_model::getAktivesPersonalNamen().
*/
public function getMitarbeiter()
{
$this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
$result = $this->MitarbeiterModel->getAktivesPersonalNamen();
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
/**
* Lists the documents of one DMS category for the contentcomponent insert dialog.
*/
public function getDmsKategorieDokumente()
{
$this->load->library('form_validation');
$this->form_validation->set_data($_GET);
$this->form_validation->set_rules('kategorie_kurzbz', 'DMS-Kategorie', 'required');
if ($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$this->load->model('content/Dms_model', 'DmsModel');
$result = $this->DmsModel->getKategorieDokumenteAlle($this->input->get('kategorie_kurzbz', TRUE));
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result) ?: array());
}
}
@@ -1,195 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class CmsAdminInhalt extends FHCAPI_Controller
{
public function __construct()
{
parent::__construct([
'getFormSchema' => ['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($this->p->t('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($this->p->t('cms', 'contentNichtGefunden'));
$this->TemplateModel->resetQuery();
$templateResult = $this->TemplateModel->load($contentData[0]->template_kurzbz);
$templateData = $this->getDataOrTerminateWithError($templateResult);
if (empty($templateData))
$this->terminateWithError($this->p->t('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()
{
$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 = [];
$lockResult = $this->cmsadminlib->getLockState($content_id, $sprache, $version);
$lock = $this->getDataOrTerminateWithError($lockResult);
if (!$lock['own'])
$this->terminateWithError($this->p->t('cms', 'speichernNurMitSperre'));
$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($this->p->t('cms', 'keineBerechtigungFuerDieseAktion'));
$contentsprache_id = $this->input->post('contentsprache_id');
$result = $this->cmsadminlib->lock($contentsprache_id);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function deleteLock()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigungFuerDieseAktion'));
$contentsprache_id = $this->input->post('contentsprache_id');
$result = $this->cmsadminlib->releaseOwnLock($contentsprache_id);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function deleteLockForced()
{
if (!$this->permissionlib->isBerechtigt('basis/cms_sperrfreigabe', 'su'))
$this->terminateWithError($this->p->t('cms', 'keineBerechtigungFuerDieseAktion'));
$contentsprache_id = $this->input->post('contentsprache_id');
$result = $this->cmsadminlib->forceRelease($contentsprache_id);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
}
@@ -1,265 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class CmsAdminStruktur extends FHCAPI_Controller
{
public function __construct()
{
parent::__construct([
'getGruppen' => ['basis/cms:r'],
'getAllGruppen' => ['basis/cms:r'],
'getChilds' => ['basis/cms:r'],
'getParents' => ['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'],
'putChildOrder' => ['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 getParents()
{
$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->getParents($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($this->p->t('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($this->p->t('cms', 'gruppeBereitsZugeordnet'));
}
$this->terminateWithSuccess(true);
}
public function deleteGruppe()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('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($this->p->t('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;
$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($this->p->t('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($this->p->t('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());
$direction = $this->input->post('direction');
$result = $this->ContentchildModel->swapSort(
$this->input->post('contentchild_id'),
$direction
);
if (isError($result))
{
// The child already sits at the end it was moved towards. That is a normal
// answer, not a failure, and the direction decides the wording.
if (getError($result) === Contentchild_model::NO_NEIGHBOUR)
{
$this->terminateWithError($this->p->t('cms',
$direction === 'up' ? 'bereitsGanzOben' : 'bereitsGanzUnten'));
}
$this->terminateWithError($this->p->t('cms', 'sortierungFehlgeschlagen'));
}
$this->terminateWithSuccess(true);
}
// Writes the whole order at once. The drag and drop moves a child over any distance,
// and putChildSort would need one request per position.
public function putChildOrder()
{
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
$this->terminateWithError($this->p->t('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());
$ids = $this->input->post('contentchild_ids');
if (is_string($ids))
$ids = json_decode($ids, true);
if (!is_array($ids) || empty($ids))
$this->terminateWithError($this->p->t('cms', 'sortierungFehlgeschlagen'));
$result = $this->ContentchildModel->setSortOrder(
$this->input->post('content_id'),
$ids
);
if (isError($result))
$this->terminateWithError($this->p->t('cms', 'sortierungFehlgeschlagen'));
$this->terminateWithSuccess(true);
}
}
@@ -51,6 +51,8 @@ class Documents extends FHCAPI_Controller
$this->loadPhrases([
'stv'
]);
$this->load->library('DocumentExportLib');
}
/**
@@ -100,23 +102,11 @@ class Documents extends FHCAPI_Controller
{
$akteExportData = $this->_getAkteExportData($xml, $xsl, $sign_user);
$akteData = $akteData['akteData'];
$exportData = $akteData['exportData'];
/**
* [
'vorlage' => $vorlage,
'xml_data' => $data,
'oe_kurzbz' => $xsl_oe_kurzbz,
'version' => $version,
'outputformat' => $outputformat,
'sign_user' => $sign_user
]
*/
$akteData = $akteExportData['akteData'];
$exportData = $akteExportData['exportData'];
// Output
$result = $this->documentexportlib->showContent(
$akteData['akteData']['inhalt'],
$contentResult = $this->documentexportlib->getContent(
$exportData['vorlage'],
$exportData['xml_data'],
$exportData['oe_kurzbz'],
@@ -125,7 +115,9 @@ class Documents extends FHCAPI_Controller
$exportData['sign_user']
);
$this->terminateWithSuccess(true);
$content = $this->getDataOrTerminateWithError($contentResult);
$this->terminateWithFileOutput($akteData['mimetype'], $content, $akteData['titel'] . '.pdf');
}
/**
@@ -161,12 +153,10 @@ class Documents extends FHCAPI_Controller
$this->load->library('form_validation');
if (!$xml) {
$xml = $this->input->post_get('xml');
$this->addMeta('xml', $xml);
$this->form_validation->set_rules('xml', 'xml', 'required');
}
if (!$xsl) {
$xsl = $this->input->post_get('xsl');
$this->addMeta('xsl', $xsl);
$this->form_validation->set_rules('xsl', 'xsl', 'required');
}
@@ -465,18 +455,13 @@ class Documents extends FHCAPI_Controller
if ($sign_user && !$vorlage->signierbar)
$this->terminateWithError($this->p->t("stv", "grades_error_sign"));
$this->load->library('DocumentExportLib');
// XML Data
$result = $this->documentexportlib->getDataURL($xml, $params);
$data = $this->getDataOrTerminateWithError($result);
$this->documentexportlib->addArchiveToData($data);
$contentResult = $this->documentexportlib->getContent($vorlage, $data, $xsl_oe_kurzbz, $version, $outputformat, $sign_user);
// Output
$result = $this->documentexportlib->getContent($vorlage, $data, $xsl_oe_kurzbz, $version, $outputformat, $sign_user);
$content = $this->getDataOrTerminateWithError($result);
$content = $this->getDataOrTerminateWithError($contentResult);
$akteData['titel'] .= '.pdf';
$akteData['inhalt'] = base64_encode($content);
@@ -494,3 +479,4 @@ class Documents extends FHCAPI_Controller
];
}
}
@@ -136,7 +136,7 @@ class ProfilUpdate extends FHCAPI_Controller
$this->uid == $profil_update->uid
) {
// Get file to be downloaded from DMS
$download = $this->dmslib->download($dms_id);
$download = $this->dmslib->getOutputFileInfo($dms_id);
$download = $this->getDataOrTerminateWithError($download);
// Download file
$this->outputFile($download);
@@ -456,24 +456,28 @@ class ProfilUpdate extends FHCAPI_Controller
$_FILES['files']['error'] = $files['error'][$i];
$_FILES['files']['size'] = $files['size'][$i];
$dms = [
"kategorie_kurzbz" => "profil_aenderung",
"version" => 0,
"name" => $_FILES['files']['name'],
"mimetype" => $_FILES['files']['type'],
"beschreibung" => $this->uid . " Profil Änderung",
"insertvon" => $this->uid,
"insertamum" => "NOW()",
];
// File upload
$uploadDataResult = $this->uploadFile('files', array('jpg', 'png', 'pdf'));
if (isError($uploadDataResult)) $this->addError(getError($uploadDataResult));
if (!hasData($uploadDataResult)) $this->addError('Upload failed');
$tmp_res = $this->dmslib->upload($dms, 'files', array("jpg", "png", "pdf"));
if(isError($tmp_res)){
$this->addError(getError($tmp_res));
if (hasData($uploadDataResult))
{
// Add file to the DMS (DB + file system)
$dms_res = $this->dmslib->add(
getData($uploadDataResult)['file_name'],
getData($uploadDataResult)['file_type'],
fopen(getData($uploadDataResult)['full_path'], 'r'),
'profil_aenderung', // kategorie_kurzbz
null, // dokument_kurzbz
null, // beschreibung
false, // cis_suche
null, // schlagworte
getAuthUID() // insertvon
);
$dms_res = $this->getDataOrTerminateWithError($dms_res);
array_push($res, $dms_res);
}
$tmp_res = $this->getDataOrTerminateWithError($tmp_res);
array_push($res, $tmp_res);
}
$this->terminateWithSuccess($res);
@@ -101,6 +101,9 @@ class Widget extends FHCAPI_Controller
$tmpsetup = json_decode($widget->setup);
$tmpsetup->file = absoluteJsImportUrl($tmpsetup->file);
$widget->setup = $tmpsetup;
$widget->permitted = empty($widget->berechtigung_kurzbz)
|| $this->permissionlib->isBerechtigt($widget->berechtigung_kurzbz);
return $widget;
}, $widgets);
@@ -28,7 +28,8 @@ class Studiensemester extends FHCAPI_Controller
'getStudienjahrByStudiensemester' => self::PERM_LOGGED,
'getAllStudiensemesterAndAktOrNext' => self::PERM_LOGGED,
'current' => self::PERM_LOGGED,
'set' => self::PERM_LOGGED
'set' => self::PERM_LOGGED,
'getContainingOrNearestByDateRange' => self::PERM_LOGGED,
)
);
// Load model StudiensemesterModel
@@ -203,4 +204,15 @@ class Studiensemester extends FHCAPI_Controller
$this->terminateWithSuccess(true);
}
public function getContainingOrNearestByDateRange()
{
$from = $this->input->get("from");
$to = $this->input->get("to");
$result = $this->StudiensemesterModel->getContainingOrNearestByDateRange($from, $to);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
}
}
@@ -158,19 +158,27 @@ class Unterbrechung extends FHCAPI_Controller
if (isset($_FILES['attachment']) && (!isset($_FILES['attachment']['error']) || $_FILES['attachment']['error'] != UPLOAD_ERR_NO_FILE)) {
$this->load->library('DmsLib');
$dms = $this->config->item('unterbrechung_dms');
if (!count(array_filter($dms, function ($v) {
return $v !== null;
})))
$dms = ['kategorie_kurzbz' => 'Akte'];
$dms['version'] = 0;
$allowed_filetypes = $this->config->item('unterbrechung_dms_filetypes') ?: ['*'];
$result = $this->dmslib->upload($dms, 'attachment', $allowed_filetypes);
$uploadDataResult = $this->uploadFile('attachment', $this->config->item('unterbrechung_dms_filetypes') ?: array('*'));
$data = $this->getDataOrTerminateWithError($result);
$dms_id = $data['dms_id'];
$data = $this->getDataOrTerminateWithError($uploadDataResult);
// Add file to the DMS (DB + file system)
$dms_res = $this->dmslib->add(
getData($uploadDataResult)['file_name'],
getData($uploadDataResult)['file_type'],
fopen(getData($uploadDataResult)['full_path'], 'r'),
'studierendenantrag',
null, // dokument_kurzbz
null, // beschreibung
false, // cis_suche
null, // schlagworte
getAuthUID() // insertvon
);
$dms_data = $this->getDataOrTerminateWithError($dms_res);
$dms_id = $dms_data->dms_id;
}
$result = $this->antraglib->createUnterbrechung($prestudent_id, $studiensemester, getAuthUID(), $grund, $datum_wiedereinstieg, $dms_id);
@@ -32,8 +32,8 @@ class Akte extends Auth_Controller
'download' => ['admin:w', 'assistenz:w'],
]);
// Load models
$this->load->model('crm/Akte_model', 'AkteModel');
// Load libraries
$this->load->library('AkteLib');
}
//------------------------------------------------------------------------------------------------------------------
@@ -49,22 +49,22 @@ class Akte extends Auth_Controller
if (!is_numeric($akte_id)) $this->terminateWithError('akte Id missing');
$result = $this->AkteModel->load($akte_id);
$akteResult = $this->aktelib->getByAkteId($akte_id);
if (!hasData($result)) $this->terminateWithError('Akte not found');
if (!hasData($akteResult)) $this->terminateWithError('Akte not found');
$data = getData($result)[0];
$data = getData($akteResult)[0];
if (isset($data->inhalt) && $data->inhalt != '')
if (!isEmptyString($data->inhalt))
{
header('Content-Description: File Transfer');
header('Content-Type: '. $data->mimetype);
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Disposition: attachment; filename="'.$data->titel.'"');
echo base64_decode($data->inhalt);
die();
$fileObj = new stdClass();
$fileObj->file_content = base64_decode($data->inhalt);
$fileObj->name = $data->filename;
$fileObj->mimetype = $data->mimetype;
$fileObj->disposition = 'attachment';
$this->outputFile($fileObj);
}
}
}
@@ -46,6 +46,9 @@ class Archiv extends FHCAPI_Controller
$this->load->model('crm/Akte_model', 'AkteModel');
$this->load->model('system/Vorlage_model', 'VorlageModel');
// Load libraries
$this->load->library('AkteLib');
// Load language phrases
$this->loadPhrases([
'archiv'
@@ -57,7 +60,7 @@ class Archiv extends FHCAPI_Controller
/**
* Get archive documents for a person
* @return void
*/
public function getArchiv()
@@ -74,7 +77,11 @@ class Archiv extends FHCAPI_Controller
$this->terminateWithValidationErrors($this->form_validation->error_array());
}
$result = $this->AkteModel->getArchiv($person_id);
$result = $this->aktelib->getByPersonId(
$person_id,
null, // dokument_kurzbz
true // archiv
);
$data = $this->getDataOrTerminateWithError($result);
@@ -94,51 +101,6 @@ class Archiv extends FHCAPI_Controller
$this->terminateWithSuccess($data);
}
/**
*
* @param
* @return object success or error
*/
public function download()
{
$akte_id = $this->input->get('akte_id');
if (!is_numeric($akte_id)) $this->terminateWithError('akte Id missing');
$result = $this->AkteModel->load($akte_id);
if (!hasData($result)) $this->terminateWithError('Akte not found');
$data = getData($result)[0];
$fileObj = new stdClass();
if (isset($data->inhalt) && $data->inhalt != '')
{
// Define handle to output stream
$tmpFilePointer = fopen("php://output", 'w');
$meta_data = stream_get_meta_data($tmpFilePointer);
$filename = $meta_data["uri"];
fwrite($tmpFilePointer, $data->inhalt);
header('Content-Description: File Transfer');
header('Content-Type: '. $data->mimetype);
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
//header('Content-Length: ' . filesize($fileObj->file));
//header("Content-type: $data->mimetype");
header('Content-Disposition: attachment; filename="'.$data->titel.'"');
readfile($filename);
die();
}
else
{
$this->load->library('AkteLib');
$result = $this->aktelib->get($akte_id);
}
}
/**
* Updating an Akte
* @return void
@@ -193,7 +155,7 @@ class Archiv extends FHCAPI_Controller
$this->addMeta('content', base64_encode($content));
}
foreach ($allowed as $field)
if ($this->input->post($field) !== null)
$data[$field] = $this->input->post($field);
@@ -248,7 +210,7 @@ class Archiv extends FHCAPI_Controller
'akte_id' => $akte_id
]));
}
$result = $this->AkteModel->delete($akte_id);
if (isError($result)) $this->terminateWithError(getError($result));
@@ -27,7 +27,8 @@ class Dokumente extends FHCAPI_Controller
// Load Libraries
$this->load->library('VariableLib', ['uid' => getAuthUID()]);
$this->load->library('form_validation');
$this->load->library('DmsLib', array('who' => getAuthUID()));
$this->load->library('AkteLib');
$this->load->library('DmsLib');
// Load language phrases
$this->loadPhrases([
@@ -184,7 +185,7 @@ class Dokumente extends FHCAPI_Controller
'titel_intern' => $this->input->post('titel_intern'),
'nachgereicht_am' => $nachgereicht_am,
'updateamum' => date('c'),
'updatevon' => $uid,
'updatevon' => getAuthUID(),
]
);
@@ -201,8 +202,6 @@ class Dokumente extends FHCAPI_Controller
if(!$dokument_kurzbz)
$this->terminateWithError($this->p->t('ui', 'errorMissingValue', ['value' => 'Dokument_kurzbz']), self::ERROR_TYPE_GENERAL);
$uid = getAuthUid();
//check if more than 1 dokumentkurzbz
//if()
@@ -210,10 +209,10 @@ class Dokumente extends FHCAPI_Controller
[
'prestudent_id' => $prestudent_id,
'dokument_kurzbz' => $dokument_kurzbz,
'mitarbeiter_uid' => $uid,
'mitarbeiter_uid' => getAuthUID(),
'datum' => date('c'),
'insertamum' => date('c'),
'insertvon' => $uid,
'insertvon' => getAuthUID(),
]
);
@@ -236,7 +235,6 @@ class Dokumente extends FHCAPI_Controller
$nachgereicht = current($dataAkte)->nachgereicht;
$inhalt = current($dataAkte)->inhalt;
$inhaltVorhanden = $inhalt != '';
$uid = getAuthUid();
$this->db->trans_start();
@@ -250,16 +248,15 @@ class Dokumente extends FHCAPI_Controller
$logdata_dms = "Logdata: " . var_export($logdata_dms, true);
//delete from dmsLib
$this->load->library('DmsLib');
$person_id = current($dataAkte)->person_id;
$result = $this->dmslib->delete($person_id, $dms_id);
$result = $this->aktelib->removeByPersonIdAndDmsId($person_id, $dms_id);
$this->getDataOrTerminateWithError($result);
//LOGGING Dms ID
$this->load->model('system/Log_model', 'LogModel');
$result = $this->LogModel->insert([
'executetime' => date('c'),
'mitarbeiter_uid' => $uid,
'mitarbeiter_uid' => getAuthUID(),
'beschreibung' => "Löschen der DMS_ID ". $dms_id,
'sql' => $logdata_dms
]);
@@ -276,7 +273,7 @@ class Dokumente extends FHCAPI_Controller
//Logging Deletion Akte
$result = $this->LogModel->insert([
'executetime' => date('c'),
'mitarbeiter_uid' => $uid,
'mitarbeiter_uid' => getAuthUID(),
'beschreibung' => "Löschen der Akte ". $akte_id,
'sql' => "DELETE FROM public.tbl_akte WHERE akte_id=" .$akte_id. " LogData: ". $logdata_akte
]);
@@ -295,7 +292,7 @@ class Dokumente extends FHCAPI_Controller
$result = $this->LogModel->insert([
'executetime' => date('c'),
'mitarbeiter_uid' => $uid,
'mitarbeiter_uid' => getAuthUID(),
'beschreibung' => "Löschen der Akte ". $akte_id,
'sql' => "DELETE FROM public.tbl_akte WHERE akte_id=" .$akte_id. " LogData: ". $logdata_akte
]);
@@ -310,7 +307,6 @@ class Dokumente extends FHCAPI_Controller
public function uploadDokument()
{
$this->load->library('DmsLib');
$prestudent_id = $this->input->post('prestudent_id');
$anmerkung_intern = $this->input->post('anmerkung_intern');
$titel_intern = $this->input->post('titel_intern');
@@ -333,34 +329,44 @@ class Dokumente extends FHCAPI_Controller
}
$this->db->trans_start();
$uid = getAuthUID();
$dms = array(
'kategorie_kurzbz' => 'Akte',
'version' => 0,
'name' => $_FILES['anhang']['name'],
'mimetype' => $_FILES['anhang']['type'],
'insertamum' => date('c'),
'insertvon' => $uid
);
$result = $this->dmslib->upload($dms, 'anhang', array("jpg", "png", "pdf"));
if (isError($result))
$uploadDataResult = $this->uploadFile('anhang', array('jpg', 'png', 'pdf'));
if (isError($uploadDataResult))
{
return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
return $this->terminateWithError(getError($uploadDataResult), self::ERROR_TYPE_GENERAL);
}
$dms_id = $result->retval['dms_id'];
// If data exists
if (hasData($uploadDataResult))
{
// Add file to the DMS (DB + file system)
$dms_res = $this->dmslib->add(
getData($uploadDataResult)['file_name'],
getData($uploadDataResult)['file_type'],
fopen(getData($uploadDataResult)['full_path'], 'r'),
'Akte', // kategorie_kurzbz
null, // dokument_kurzbz
null, // beschreibung
false, // cis_suche
null, // schlagworte
getAuthUID() // insertvon
);
}
// Error occurred or data not found
if (isError($dms_res) || !hasData($dms_res)) $this->terminateWithError(getError($dms_res), self::ERROR_TYPE_GENERAL);
$dms_id = getData($dms_res)->dms_id;
$person_id = $this->_getPersonId($prestudent_id);
$result = $this->DokumentModel->load($dokument_kurzbz);
$data = $this->getDataOrTerminateWithError($result);
$dokumentResult = $this->DokumentModel->load($dokument_kurzbz);
$data = $this->getDataOrTerminateWithError($dokumentResult);
$bezeichnung = current($data)->bezeichnung;
//save entry in akte
if($dms_id)
// Save entry in akte
if ($dms_id)
{
$result = $this->AkteModel->insert([
'person_id' => $person_id,
@@ -369,7 +375,7 @@ class Dokumente extends FHCAPI_Controller
'mimetype' => $_FILES['anhang']['type'],
'insertamum' => date('c'),
'erstelltam' => date('c'),
'insertvon' => $uid,
'insertvon' => getAuthUID(),
'anmerkung_intern' => $anmerkung_intern,
'titel_intern' => $titel_intern,
'bezeichnung' => $bezeichnung,
@@ -416,28 +422,26 @@ class Dokumente extends FHCAPI_Controller
$filecontentbase64 = $data->inhalt;
$filename = $data->titel;
if(intval($data->dms_id) > 0)
if (intval($data->dms_id) > 0)
{
$dmsdokres = $this->dmslib->read($data->dms_id);
if (!hasData($dmsdokres)) $this->terminateWithError('DMS File not found');
$dmsdok = getData($dmsdokres)[0];
$file = $this->dmslib->getOutputFileInfo($data->dms_id, $filename);
$mimetype = $dmsdok->mimetype;
$filecontentbase64 = $dmsdok->file_content;
$filename = $dmsdok->name;
$this->terminateWithFileOutput(getData($file)->mimetype, file_get_contents(getData($file)->file), $filename);
}
else
{
$filecontent = '';
$filecontent = '';
if (!empty($filecontentbase64)) {
$filecontent = base64_decode($filecontentbase64, true);
if (!empty($filecontentbase64)) {
$filecontent = base64_decode($filecontentbase64, true);
if ($filecontent === false) {
$this->terminateWithError('Base64-Dekodierung failed.');
if ($filecontent === false) {
$this->terminateWithError('Base64-Dekodierung failed.');
}
}
}
$this->terminateWithFileOutput($mimetype, $filecontent, $filename);
$this->terminateWithFileOutput($mimetype, $filecontent, $filename);
}
}
private function _getMissingDocuments($studiengang_kz, $prestudent_id)
@@ -753,10 +757,7 @@ class Dokumente extends FHCAPI_Controller
);
$data = $this->getDataOrTerminateWithError($result);
$student = current($data);
return $student->prestudent_id;
return hasData($data) ? getData($data)[0]->prestudent_id : null;
}
/**
@@ -33,11 +33,6 @@ class Notiz extends Notiz_Controller
]);
}
/* public function getUid()
{
$this->terminateWithSuccess(getAuthUID());
}*/
public function getNotizen($id, $type)
{
@@ -59,317 +54,6 @@ class Notiz extends Notiz_Controller
}
/* public function loadNotiz()
{
$_POST = json_decode(utf8_encode($this->input->raw_input_stream), true);
$notiz_id = $this->input->post('notiz_id');
//$this->load->model('person/Notiz_model', 'NotizModel');
$this->NotizModel->addJoin('public.tbl_notiz_dokument', 'notiz_id', 'LEFT');
$this->NotizModel->addSelect('*');
$this->NotizModel->addSelect("TO_CHAR(CASE WHEN public.tbl_notiz.updateamum >= public.tbl_notiz.insertamum
THEN public.tbl_notiz.updateamum ELSE public.tbl_notiz.insertamum END::timestamp, 'DD.MM.YYYY HH24:MI:SS') AS lastUpdate");
$this->NotizModel->addLimit(1);
$result = $this->NotizModel->loadWhere(
array('notiz_id' => $notiz_id)
);
if (isError($result))
{
$this->terminateWithError($result, self::ERROR_TYPE_GENERAL);
}
elseif (!hasData($result))
{
$this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=>'Notiz_id']), self::ERROR_TYPE_GENERAL);
}
else
{
$this->terminateWithSuccess(current(getData($result)));
}
}
public function updateNotiz()
{
$this->load->library('form_validation');
$this->load->library('DmsLib');
if (isset($_POST['data']))
{
$data = json_decode($_POST['data']);
unset($_POST['data']);
foreach ($data as $k => $v) {
$_POST[$k] = $v;
}
}
$notiz_id = $this->input->post('notiz_id');
if(!$notiz_id)
{
$this->terminateWithError($this->p->t('ui','error_missingId',['id'=>'Notiz_id']), self::ERROR_TYPE_GENERAL);
}
//Form Validation
$this->form_validation->set_rules('titel', 'Titel', 'required', [
'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'Titel'])
]);
$this->form_validation->set_rules('text', 'Text', 'required', [
'required' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'Text'])
]);
if ($this->form_validation->run() == false)
{
$this->terminateWithValidationErrors($this->form_validation->error_array());
}
//update Notiz
$uid = getAuthUID();
$titel = $this->input->post('titel');
$text = $this->input->post('text');
$verfasser_uid = $this->input->post('verfasser');
$bearbeiter_uid = isset($_POST['bearbeiter']) ? $_POST['bearbeiter'] : $uid;
$erledigt = $this->input->post('erledigt');
$start = $this->input->post('start');
$ende = $this->input->post('ende');
$result = $this->NotizModel->update(
[
'notiz_id' => $notiz_id
],
[
'titel' => $titel,
'updatevon' => $uid,
'updateamum' => date('c'),
'text' => $text,
'verfasser_uid' => $verfasser_uid,
'bearbeiter_uid' => $bearbeiter_uid,
'start' => $start,
'ende' => $ende,
'erledigt' => $erledigt
]
);
if (isError($result))
{
return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
}
//update(1) laden aller bereits mit dieser notiz_id verknüpften DMS-Einträge
$this->load->model('person/Notizdokument_model', 'NotizdokumentModel');
$this->NotizdokumentModel->addJoin('campus.tbl_dms_version', 'dms_id');
$dms_uploaded = null;
$result = $this->NotizdokumentModel->loadWhere(array('notiz_id' => $notiz_id));
if (isError($result))
{
$this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
}
elseif (!hasData($result))
{
$dms_id_arr = null;
}
else
{
$result = getData($result);
foreach($result as $doc) {
$dms_id_arr[] = array(
'name' => $doc->name,
'dms_id' => $doc->dms_id
);
}
}
foreach ($_FILES as $k => $file)
{
//update(2) alle neuen files (alle außer type application/x.fhc-dms+json) anhängen
if($file["type"] == 'application/x.fhc-dms+json')
{
$dms_uploaded[] = array(
'name' => $file["name"]
);
}
else
{
$dms = array(
'kategorie_kurzbz' => 'notiz',
'version' => 0,
'name' => $file["name"],
'mimetype' => $file["type"],
'insertamum' => date('c'),
'insertvon' => $uid
);
//Todo(manu) check if filetypes weiter eingeschränkt werden sollen
//Todo(manu)check name files: nicht gleiches file 2mal hochladen
$result = $this->dmslib->upload($dms, $k, array('*'));
if (isError($result))
{
return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
}
$dms_id = $result->retval['dms_id'];
$result = $this->NotizdokumentModel->insert(array('notiz_id' => $notiz_id, 'dms_id' => $dms_id));
if (isError($result))
{
return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
}
}
}
//update(3) check if Dateien gelöscht wurden
if(count($dms_uploaded) != count($dms_id_arr))
{
if (count($dms_uploaded) == 0)
{
$filesDeleted = $dms_id_arr;
}
else
{
$upload_new_names = array_column($dms_uploaded, "name");
$filesDeleted = array_filter($dms_id_arr, function ($file) use ($upload_new_names) {
return !in_array($file["name"], $upload_new_names);
});
}
foreach ($filesDeleted as $file)
{
$result = $this->dmslib->removeAll($file['dms_id']);
if (isError($result))
{
return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
}
else
$this->outputJson($result);
}
}
return $this->terminateWithSuccess($result);
}*/
/* public function deleteNotiz()
{
$_POST = json_decode(utf8_encode($this->input->raw_input_stream), true);
$notiz_id = $this->input->post('notiz_id');
$type = $this->input->post('type_id');
$id = $this->input->post('id');
//dms_id auslesen aus notizdokument wenn vorhanden
$dms_id_arr = [];
$this->load->model('person/Notizdokument_model', 'NotizdokumentModel');
$result = $this->NotizdokumentModel->loadWhere(array('notiz_id' => $notiz_id));
if (isError($result))
{
return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
}
if(hasData($result))
{
$result = getData($result);
foreach ($result as $doc) {
$dms_id_arr[] = $doc->dms_id;
}
}
if($dms_id_arr)
{
$this->load->library('DmsLib');
foreach($dms_id_arr as $dms_id)
{
$result = $this->dmslib->removeAll($dms_id);
if (isError($result))
{
return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
}
$this->outputJson($result);
}
}
//delete Notizzuordnung
if($type == "software_id")
{
// Loads extension Model
$this->load->model('extensions/FHC-Core-Softwarebereitstellung/Softwarenotizzuordnung_model', 'ExtensionnotizzuordnungModel');
$result = $this->ExtensionnotizzuordnungModel->delete([
'notiz_id' => $notiz_id,
'id' => strval($id)
],
[
'type_id' => $type
]);
}
else
{
//notizzuordnungsid!
$result = $this->NotizzuordnungModel->delete(['notiz_id' => $notiz_id, $type => $id]);
}
$this->load->model('person/Notiz_model', 'NotizModel');
//$this->NotizModel->addJoin('public.tbl_notizzuordnung', 'notiz_id');
//TODO (erweitern um Type_id) für Extensions, damit auch Notizzuordnung gelöscht werden kann
//Löschen von Notiz
$result = $this->NotizModel->delete(
array('notiz_id' => $notiz_id)
);
if (isError($result))
{
return $this->terminateWithError($result, self::ERROR_TYPE_GENERAL);
}
if(!hasData($result))
{
return $this->terminateWithError($this->p->t('ui','error_missingId', ['id'=> 'Notiz_id']), self::ERROR_TYPE_GENERAL);
}
return $this->terminateWithSuccess(current(getData($result)));
}*/
/* public function loadDokumente()
{
$_POST = json_decode(utf8_encode($this->input->raw_input_stream), true);
$notiz_id = $this->input->post('notiz_id');
$this->NotizModel->addSelect('campus.tbl_dms_version.*');
$this->NotizModel->addJoin('public.tbl_notiz_dokument', 'ON (public.tbl_notiz_dokument.notiz_id = public.tbl_notiz.notiz_id)');
$this->NotizModel->addJoin('campus.tbl_dms_version', 'ON (public.tbl_notiz_dokument.dms_id = campus.tbl_dms_version.dms_id)');
$result = $this->NotizModel->loadWhere(
array('public.tbl_notiz.notiz_id' => $notiz_id)
);
if (isError($result)) {
return $this->terminateWithError($result, self::ERROR_TYPE_GENERAL);
}
if(!hasData($result))
{
return $this->terminateWithError($this->p->t('ui','error_missingId', ['id'=> 'Notiz_id']), self::ERROR_TYPE_GENERAL);
}
return $this->terminateWithSuccess(getData($result));
}*/
/* public function getMitarbeiter($searchString)
{
$this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
$result = $this->MitarbeiterModel->searchMitarbeiter($searchString);
if (isError($result)) {
$this->terminateWithError($result, self::ERROR_TYPE_GENERAL);
}
return $this->terminateWithSuccess($result);
}*/
public function isBerechtigt($id, $typeId)
{
if(!$this->permissionlib->isBerechtigt('admin', 'suid') && !$this->permissionlib->isBerechtigt('assistenz', 'suid'))
@@ -379,7 +63,6 @@ class Notiz extends Notiz_Controller
return $this->terminateWithError($result, self::ERROR_TYPE_GENERAL);
}
return success("berechtigt in überschreibender Funktion");
/* return $this->terminateWithError('keine Berechtigung bro', self::ERROR_TYPE_GENERAL);*/
}
}
}
@@ -73,7 +73,7 @@ class Attachment extends FHC_Controller
}
}
$result = $this->dmslib->download($dms_id);
$result = $this->dmslib->getOutputFileInfo($dms_id);
if (isError($result))
return show_error(getError($result));
@@ -393,7 +393,7 @@ class approveAnrechnungDetail extends Auth_Controller
$filename = $this->anrechnunglib->setFilenameOnDownload($dms_id);
// Get file to be downloaded from DMS
$download = $this->dmslib->download($dms_id, $filename);
$download = $this->dmslib->getOutputFileInfo($dms_id, $filename);
if (isError($download)) return $download;
// Download file
@@ -282,7 +282,7 @@ class approveAnrechnungUebersicht extends Auth_Controller
$filename = $this->anrechnunglib->setFilenameOnDownload($dms_id);
// Get file to be downloaded from DMS
$download = $this->dmslib->download($dms_id, $filename);
$download = $this->dmslib->getOutputFileInfo($dms_id, $filename);
if (isError($download)) return $download;
// Download file
@@ -150,15 +150,12 @@ class CreateAnrechnung extends Auth_Controller
}
// Upload document
$result = self::_uploadFile();
$result = $this->_uploadFile();
if (isError($result))
{
$this->terminateWithJsonError($result->retval);
}
if (isError($result)) $this->terminateWithJsonError(getError($result));
// Hold just inserted DMS ID
$lastInsert_dms_id = $result->retval['dms_id'];
$lastInsert_dms_id = getData($result)->dms_id;
// Save Anrechnung and Anrechnungstatus
$result = $this->AnrechnungModel->createAnrechnungsantrag(
@@ -227,17 +224,25 @@ class CreateAnrechnung extends Auth_Controller
*/
private function _uploadFile()
{
$dms = array(
'kategorie_kurzbz' => 'anrechnung',
'version' => 0,
'name' => $_FILES['uploadfile']['name'],
'mimetype' => $_FILES['uploadfile']['type'],
'insertamum' => (new DateTime())->format('Y-m-d H:i:s'),
'insertvon' => $this->_uid
);
// Upload document
return $this->dmslib->upload($dms, 'uploadfile', array('pdf'));
$uploadDataResult = $this->uploadFile('uploadfile', array('pdf'));
// If an error occurred while uploading the file
if (isError($uploadDataResult)) return $uploadDataResult;
if (!hasData($uploadDataResult)) return error('Upload failed');
// Add file to the DMS (DB + file system)
return $this->_ci->dmslib->add(
getData($uploadDataResult)['file_name'],
getData($uploadDataResult)['file_type'],
fopen(getData($uploadDataResult)['full_path'], 'r'),
'anrechnung', // kategorie_kurzbz
null, // dokument_kurzbz
null, // beschreibung
false, // cis_suche
null, // schlagworte
getAuthUID() // insertvon
);
}
private function _LVhasBlockingGrades($studiensemester_kurzbz, $lehrveranstaltung_id, $prestudent_id)
@@ -268,4 +273,4 @@ class CreateAnrechnung extends Auth_Controller
return false;
}
}
}
@@ -83,7 +83,7 @@ class requestAnrechnung extends Auth_Controller
$is_expired = $this->_isExpired($studiensemester_kurzbz);
// Check if Lehrveranstaltung was already graded with application blocking grades
$is_blocked = self::_LVhasBlockingGrades($studiensemester_kurzbz, $lehrveranstaltung_id);
$is_blocked = $this->_LVhasBlockingGrades($studiensemester_kurzbz, $lehrveranstaltung_id);
// Get Anrechung data
$anrechnungData = $this->anrechnunglib->getAnrechnungDataByLv($lehrveranstaltung_id, $studiensemester_kurzbz, $prestudent_id);
@@ -152,27 +152,24 @@ class requestAnrechnung extends Auth_Controller
$prestudent_id = getData($result)[0]->prestudent_id;
// Exit if application already exists
if (self::_applicationExists($lehrveranstaltung_id, $studiensemester_kurzbz, $prestudent_id))
if ($this->_applicationExists($lehrveranstaltung_id, $studiensemester_kurzbz, $prestudent_id))
{
return $this->outputJsonError($this->p->t('anrechnung', 'antragBereitsGestellt'));
}
// Exit if application is a past ( < actual ) studysemester
if (self::_applicationIsPastSS($studiensemester_kurzbz))
if ($this->_applicationIsPastSS($studiensemester_kurzbz))
{
return $this->outputJsonError($this->p->t('anrechnung', 'antragNichtFuerVerganganeSS'));
}
// Upload document
$result = self::_uploadFile();
$result = $this->_uploadFile();
if (isError($result))
{
return $this->outputJsonError($result->retval);
}
if (isError($result)) return $this->outputJsonError(getError($result));
// Hold just inserted DMS ID
$lastInsert_dms_id = $result->retval['dms_id'];
$lastInsert_dms_id = getData($result)->dms_id;
// Save Anrechnung and Anrechnungstatus
$result = $this->AnrechnungModel->createAnrechnungsantrag(
@@ -182,8 +179,8 @@ class requestAnrechnung extends Auth_Controller
$begruendung_id,
$lastInsert_dms_id,
$anmerkung,
$begruendung_ects,
$begruendung_lvinhalt
$begruendung_ects,
$begruendung_lvinhalt
);
if (isError($result))
@@ -215,11 +212,11 @@ class requestAnrechnung extends Auth_Controller
$this->_checkIfEntitledToReadDMSDoc($dms_id);
// Get file to be downloaded from DMS
$download = $this->dmslib->download($dms_id);
if (isError($download)) return $download;
$download = $this->dmslib->getOutputFileInfo($dms_id);
if (isError($download)) return $download;
// Download file
$this->outputFile(getData($download));
// Download file
$this->outputFile(getData($download));
}
/**
@@ -367,16 +364,25 @@ class requestAnrechnung extends Auth_Controller
*/
private function _uploadFile()
{
$dms = array(
'kategorie_kurzbz' => 'anrechnung',
'version' => 0,
'name' => $_FILES['uploadfile']['name'],
'mimetype' => $_FILES['uploadfile']['type'],
'insertamum' => (new DateTime())->format('Y-m-d H:i:s'),
'insertvon' => $this->_uid
);
// Upload document
return $this->dmslib->upload($dms, 'uploadfile', array('pdf'));
$uploadDataResult = $this->uploadFile('uploadfile', array('pdf'));
// If an error occurred while uploading the file
if (isError($uploadDataResult)) return $uploadDataResult;
// If an error occurred while uploading the file
if (!hasData($uploadDataResult)) return error('Upload failed');
// Add file to the DMS (DB + file system)
return $this->_ci->dmslib->add(
getData($uploadDataResult)['file_name'],
getData($uploadDataResult)['file_type'],
fopen(getData($uploadDataResult)['full_path'], 'r'),
'anrechnung',
null, // dokument_kurzbz
null, // beschreibung
false, // cis_suche
null, // schlagworte
getAuthUID() // insertvon
);
}
}
@@ -237,7 +237,7 @@ class reviewAnrechnungDetail extends Auth_Controller
$filename = $this->anrechnunglib->setFilenameOnDownload($dms_id);
// Get file to be downloaded from DMS
$download = $this->dmslib->download($dms_id, $filename);
$download = $this->dmslib->getOutputFileInfo($dms_id, $filename);
if (isError($download)) return $download;
// Download file
@@ -196,7 +196,7 @@ class reviewAnrechnungUebersicht extends Auth_Controller
$filename = $this->anrechnunglib->setFilenameOnDownload($dms_id);
// Get file to be downloaded from DMS
$download = $this->dmslib->download($dms_id, $filename);
$download = $this->dmslib->getOutputFileInfo($dms_id, $filename);
if (isError($download)) return $download;
// Download file
@@ -183,6 +183,7 @@ class InfoCenter extends Auth_Controller
// Loads libraries
$this->load->library('PersonLogLib');
$this->load->library('WidgetLib');
$this->load->library('AkteLib');
$this->load->config('infocenter');
@@ -456,8 +457,7 @@ class InfoCenter extends Auth_Controller
if (isset($akte_id) && isset($person_id))
{
$this->load->library('AkteLib');
$akte = $this->aktelib->get($akte_id);
$akte = $this->aktelib->getByAkteId($akte_id);
if (hasData($akte))
{
@@ -1173,8 +1173,6 @@ class InfoCenter extends Auth_Controller
*/
public function outputAkteContent($akte_id)
{
$this->load->library('DmsLib');
$akte = $this->AkteModel->load($akte_id);
if (isError($akte))
@@ -1182,7 +1180,7 @@ class InfoCenter extends Auth_Controller
show_error(getError($akte));
}
$aktecontent = $this->dmslib->getAkteContent($akte_id);
$aktecontent = $this->aktelib->getByAkteId($akte_id);
if (isError($aktecontent))
{
@@ -2486,4 +2484,5 @@ class InfoCenter extends Auth_Controller
$this->loglib->logError('Studiengang has no mail for sending Freigabe mail');
}
}
}
}
+121 -19
View File
@@ -136,37 +136,77 @@ abstract class FHC_Controller extends CI_Controller
/**
* To download the given file represented by the fileObj parameter.
* fileObj has the following structure:
* $fileObj->filename
* $fileObj->file
* $fileObj->file OR $fileObj->file_content
* $fileObj->name
* $fileObj->mimetype
* $fileObj->disposition
* $fileObj->disposition (inline OR attachment)
*/
protected function outputFile($fileObj)
{
// If the file exists
if (isset($fileObj->file) && !isEmptyString($fileObj->file) && file_exists($fileObj->file))
if ((isset($fileObj->file) && !isEmptyString($fileObj->file) && file_exists($fileObj->file))
|| (isset($fileObj->file_content) && !isEmptyString($fileObj->file_content)))
{
header('Content-Description: File Transfer');
header('Content-Type: '. $fileObj->mimetype);
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($fileObj->file));
$content_length = 0;
if (isset($fileObj->disposition)
&& ($fileObj->disposition == 'inline' || $fileObj->disposition == 'attachment'))
// If file content has been provided
if (isset($fileObj->file_content) && !isEmptyString($fileObj->file_content))
{
header('Content-Disposition: '. $fileObj->disposition. '; filename="'. $fileObj->name. '"');
$content_length = strlen($fileObj->file_content);
}
else // otherwise the path + name of the file
{
$content_length = filesize($fileObj->file);
}
readfile($fileObj->file); // reads the file content to the output buffer
}
else
{
// Otherwise print an error
show_error('The provided file does not exist: '.(isset($fileObj->file) ? $fileObj->file : 'file not given'));
header('Content-Description: File Transfer');
header('Content-Type: '. $fileObj->mimetype);
header('Content-Length: ' . $content_length);
header('Content-Transfer-Encoding: binary');
header('Content-Disposition: '. $fileObj->disposition. '; filename="'. $fileObj->name. '"');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Expires: ' . date("D, d M Y H:i:s", time()));
header('Pragma: public');
// Clean the output buffer
flush();
// If file content has been provided
if (isset($fileObj->file_content) && !isEmptyString($fileObj->file_content))
{
echo $fileObj->file_content;
}
else // otherwise get the content from file system
{
readfile($fileObj->file);
}
// Terminate the execution
exit();
}
// Otherwise return an error
show_error('The provided file does not exist or file content is empty');
}
/**
*
*/
protected function outputImageByContent($mimetype, $file_content)
{
if (isEmptyString($file_content)) show_error('The provided file content is not valid');
$this->_outputImage($mimetype, $file_content);
}
/**
*
*/
protected function outputImageByFile($mimetype, $file_name)
{
if (!file_exists($file_name)) show_error('The provided file does not exist');
$this->_outputImage($mimetype, null, $file_name);
}
/**
@@ -178,6 +218,29 @@ abstract class FHC_Controller extends CI_Controller
return json_decode($this->input->raw_input_stream);
}
/**
* Utility function to upload a file
* - post_field_name: the name of the field in the HTTP POST payload, this is also the index in the super global $_FILES array
* - allowed_types: a list of file extensions that are allowed to be uploaded (ex. array('pdf', 'odt') or array('jpg', 'jpeg', 'gif')
*/
protected function uploadFile($post_field_name, $allowed_types = array('*'))
{
$this->load->library(
'upload',
array(
'upload_path' => sys_get_temp_dir(),
'allowed_types' => $allowed_types,
'overwrite' => true
)
);
// If the upload was a success then return the uploaded file info
if ($this->upload->do_upload($post_field_name)) return success($this->upload->data());
// If an error occurred then return it without tags
return error($this->upload->display_errors('', ''));
}
//------------------------------------------------------------------------------------------------------------------
// Private methods
@@ -193,5 +256,44 @@ abstract class FHC_Controller extends CI_Controller
show_error('This web site cannot work correctly without the HTTPS protocol enabled');
}
}
/**
*
*/
private function _outputImage($mimetype, $file_content = null, $file_name = null)
{
$content_length = 0;
// If file content has been provided
if ($file_content != null)
{
$content_length = strlen($file_content);
}
else // otherwise the path + name of the file
{
$content_length = filesize($file_name);
}
header('Content-Type: '. $mimetype);
header('Content-Length: ' . $content_length);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Expires: ' . date("D, d M Y H:i:s", time()));
// Clean the output buffer
flush();
// If file content has been provided
if ($file_content != null)
{
echo $file_content;
}
else // otherwise get the content from file system
{
readfile($file_name);
}
// Terminate the execution
exit();
}
}
+57 -26
View File
@@ -196,26 +196,39 @@ abstract class Notiz_Controller extends FHCAPI_Controller
$dms_id_arr = [];
foreach ($_FILES as $k => $file)
{
$dms = array(
'kategorie_kurzbz' => 'notiz',
'version' => 0,
'name' => $file["name"],
'mimetype' => $file["type"],
'insertamum' => date('c'),
'insertvon' => $uid
);
//Todo(manu) check if filetypes weiter eingeschränkt werden sollen
//Todo(manu)check name files: nicht gleiches file 2mal hochladen
//Todo define in dms component: readFile, downloadFile
$result = $this->dmslib->upload($dms, $k, ['*']);
/* $result = $this->dmslib->upload($dms, $k, ['application/pdf','application/x.fhc-dms+json']);*/
if (isError($result))
$uploadDataResult = $this->uploadFile($k);
if (isError($uploadDataResult))
{
$this->db->trans_rollback();
$this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
$this->terminateWithError(getError($uploadDataResult), self::ERROR_TYPE_GENERAL);
}
$dms_id_arr[] = $result->retval['dms_id'];
if (!hasData($uploadDataResult))
{
$this->db->trans_rollback();
$this->terminateWithError('Upload failed', self::ERROR_TYPE_GENERAL);
}
// Add file to the DMS (DB + file system)
$addResult = $this->dmslib->add(
getData($uploadDataResult)['file_name'],
getData($uploadDataResult)['file_type'],
fopen(getData($uploadDataResult)['full_path'], 'r'),
'notiz', // kategorie_kurzbz
null, // dokument_kurzbz
null, // beschreibung
false, // cis_suche
null, // schlagworte
getAuthUID() // insertvon
);
// If error occurred
if (isError($addResult) || !hasData($addResult))
$this->terminateWithError(getError($addResult), self::ERROR_TYPE_GENERAL);
$dms_id_arr[] = getData($addResult)->dms_id;
}
//save entry in Notizdokument
@@ -319,22 +332,40 @@ abstract class Notiz_Controller extends FHCAPI_Controller
}
else
{
$dms = array(
'kategorie_kurzbz' => 'notiz',
'version' => 0,
'name' => $file["name"],
'mimetype' => $file["type"],
'insertamum' => date('c'),
'insertvon' => $uid
);
//Todo(manu) check if filetypes weiter eingeschränkt werden sollen
//Todo(manu)check name files: nicht gleiches file 2mal hochladen
//Todo define in dms component: readFile, downloadFile
$result = $this->dmslib->upload($dms, $k, array('*'));
$uploadDataResult = $this->uploadFile($k);
if (isError($uploadDataResult))
{
$this->db->trans_rollback();
$this->terminateWithError(getError($uploadDataResult), self::ERROR_TYPE_GENERAL);
}
if (!hasData($uploadDataResult))
{
$this->db->trans_rollback();
$this->terminateWithError('Upload failed', self::ERROR_TYPE_GENERAL);
}
$result = $this->getDataOrTerminateWithError($result);
$dms_id = $result['dms_id'];
// Add file to the DMS (DB + file system)
$addResult = $this->dmslib->add(
getData($uploadDataResult)['file_name'],
getData($uploadDataResult)['file_type'],
fopen(getData($uploadDataResult)['full_path'], 'r'),
'notiz', // kategorie_kurzbz
null, // dokument_kurzbz
null, // beschreibung
false, // cis_suche
null, // schlagworte
getAuthUID() // insertvon
);
// If error occurred
if (isError($addResult) || !hasData($addResult))
$this->terminateWithError(getError($addResult), self::ERROR_TYPE_GENERAL);
$addResult = $this->getDataOrTerminateWithError($addResult);
$dms_id = $addResult['dms_id'];
$result = $this->NotizdokumentModel->insert(array('notiz_id' => $post_data['notiz_id'], 'dms_id' => $dms_id));
+4 -4
View File
@@ -418,10 +418,10 @@ function findResource($path, $resource, $subdir = false, $extraDir = null)
if (!function_exists('array_is_list')) {
function array_is_list(array $arr)
{
if ($arr === []) {
return true;
}
return array_keys($arr) === range(0, count($arr) - 1);
if ($arr === []) {
return true;
}
return array_keys($arr) === range(0, count($arr) - 1);
}
}
+203 -124
View File
@@ -1,12 +1,21 @@
<?php
/**
* FH-Complete
/* Copyright (C) 2025 fhcomplete.net
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*
* @package FHC-Helper
* @author FHC-Team
* @copyright Copyright (c) 2022 fhcomplete.net
* @license GPLv3
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
@@ -121,90 +130,32 @@ class AkteLib
* Gets akte data and associated dms data by akte Id
* Returns success with akte and dms data or error
*/
public function get($akte_id)
public function getByAkteId($akte_id, $dokument_kurzbz = null, $archiv = null, $signiert = null, $stud_selfservice = null)
{
// get Akte data
$this->_ci->AkteModel->addSelect('person_id, dokument_kurzbz, mimetype, erstelltam, titel, bezeichnung,
gedruckt, uid, dms_id, nachgereicht, nachgereicht_am, anmerkung,
ausstellungsnation, formal_geprueft_amum, archiv, signiert,
stud_selfservice, akzeptiertamum, insertvon, insertamum, updatevon, updateamum');
$this->_ci->AkteModel->load($akte_id);
$akteResult = $this->_ci->AkteModel->load($akte_id);
if (isError($akteResult)) return $akteResult;
if (!hasData($akteResult)) return error("Akte not found");
$resultObject = getData($akteResult)[0];
// set properties with same name in Akte and Dms table
$resultObject->akte_mimetype = $resultObject->mimetype;
// get dms data
$dmsResult = $this->_ci->dmslib->getLastVersion($resultObject->dms_id);
if (isError($dmsResult)) return $dmsResult;
// properties to retrieve from dms
$dmsProperties = array('version', 'filename', 'mimetype', 'name', 'beschreibung', 'cis_suche', 'schlagworte', DmsLib::FILE_CONTENT_PROPERTY);
// set dms properties
if (hasData($dmsResult))
{
$dmsData = getData($dmsResult);
foreach ($dmsProperties as $dmsProperty)
{
$resultObject->{$dmsProperty} = $dmsData->{$dmsProperty};
}
}
else
{
// set null if no dms result found
foreach ($dmsProperties as $dmsProperty)
{
$resultObject->{$dmsProperty} = null;
}
}
// return the object containing akte and dms data
return success($resultObject);
return $this->_get(
$akte_id,
null, // person_id
$dokument_kurzbz,
$archiv,
$signiert,
$stud_selfservice
);
}
/**
* Gets Akte data and associated dms data by person Id and dokument_kurzbz
* Returns success with result array with akte and dms data or error
*/
public function getByPersonIdAndDocumentType($person_id, $dokument_kurzbz)
public function getByPersonId($person_id, $dokument_kurzbz = null, $archiv = null, $signiert = null, $stud_selfservice = null)
{
// load all Akte entries for given person and dokument_kurzbz
$this->_ci->AkteModel->addSelect('akte_id');
$akteResult = $this->_ci->AkteModel->loadWhere(
array(
'person_id' => $person_id,
'dokument_kurzbz' => $dokument_kurzbz
)
return $this->_get(
null, // akte_id
$person_id,
$dokument_kurzbz,
$archiv,
$signiert,
$stud_selfservice
);
if (!hasData($akteResult)) return error("Akte not found");
$akteData = getData($akteResult);
$resultArr = array();
// for each found akte entry
foreach ($akteData as $akte)
{
// get dms and akte data from akte Id
$getAkteDmsResult = $this->get($akte->akte_id);
if (isError($getAkteDmsResult)) return $getAkteDmsResult;
$resultArr[] = getData($getAkteDmsResult);
}
// return all found entries
return success($resultArr);
}
/**
@@ -213,56 +164,184 @@ class AkteLib
*/
public function remove($akte_id)
{
// get dms_id for akte
return $this->_remove($akte_id, null, null);
}
/**
* Removes Akte by $person_id and $dms_id, removes all associated dms entries and versions, and deletes all associated files
* Returns success with removed version numbers or error
*/
public function removeByPersonIdAndDmsId($person_id, $dms_id)
{
return $this->_remove(null, $person_id, $dms_id);
}
/**
*
*/
private function _get($akte_id = null, $person_id = null, $dokument_kurzbz = null, $archiv = null, $signiert = null, $stud_selfservice = null)
{
$dbModel = new DB_Model();
$query = 'SELECT akte_id,
person_id,
dokument_kurzbz,
inhalt,
CASE WHEN inhalt is not null THEN true ELSE false END as inhalt_vorhanden,
mimetype,
erstelltam,
gedruckt,
titel,
bezeichnung,
updateamum,
updatevon,
insertamum,
insertvon,
uid,
dms_id,
nachgereicht,
anmerkung,
titel_intern,
anmerkung_intern,
nachgereicht_am,
ausstellungsnation,
formal_geprueft_amum,
archiv,
signiert,
stud_selfservice,
akzeptiertamum
FROM public.tbl_akte
WHERE TRUE';
// Query parameters
$paramArray = array();
// akte_id
if (is_numeric($akte_id) || is_array($akte_id))
{
$paramArray[] = $akte_id;
if (is_numeric($akte_id)) $query .= ' AND akte_id = ?';
if (is_array($akte_id)) $query .= ' AND akte_id IN ?';
}
// person_id
if (is_numeric($person_id) || is_array($person_id))
{
$paramArray[] = $person_id;
if (is_numeric($person_id)) $query .= ' AND person_id = ?';
if (is_array($person_id)) $query .= ' AND person_id IN ?';
}
// dokument_kurzbz
if (!isEmptyString($dokument_kurzbz))
{
$paramArray[] = $dokument_kurzbz;
$query .= ' AND dokument_kurzbz = ?';
}
// archiv
if (is_bool($archiv))
{
$paramArray[] = $archiv;
$query .= ' AND archiv = ?';
}
// signiert
if (is_bool($signiert))
{
$paramArray[] = $signiert;
$query .= ' AND signiert = ?';
}
// stud_selfservice
if (is_bool($stud_selfservice))
{
$paramArray[] = $stud_selfservice;
$query .= ' AND stud_selfservice = ?';
}
// If no parameters has been provided exit
if (isEmptyArray($paramArray)) return error('Called without giving any parameter');
// Loads data from DB
$akteResult = $dbModel->execReadOnlyQuery($query, $paramArray);
// If error or data not found then exit
if (isError($akteResult)) return $akteResult;
if (!hasData($akteResult)) return error('Akte not found');
// For each record from the akte
foreach (getData($akteResult) as $resultObject)
{
// get dms data
$dmsResult = $this->_ci->dmslib->getLastVersion($resultObject->dms_id);
if (isError($dmsResult)) return $dmsResult;
// properties to retrieve from dms
$dmsProperties = array('version', 'filename', 'mimetype', 'name', 'beschreibung', 'cis_suche', 'schlagworte');
// set dms properties
if (hasData($dmsResult))
{
$dmsData = getData($dmsResult)[0];
foreach ($dmsProperties as $dmsProperty)
{
// If the property is _not_ 'mimetype' _or_
// If the mimetype from the akte table is null then overwrite it with the one from the DMS
if ($dmsProperty != 'mimetype' || ($dmsProperty == 'mimetype' && $resultObject->{$dmsProperty} == null))
{
$resultObject->{$dmsProperty} = $dmsData->{$dmsProperty};
}
}
}
else
{
// Set null if no dms result found
foreach ($dmsProperties as $dmsProperty)
{
if ($dmsProperty != 'mimetype') $resultObject->{$dmsProperty} = null;
}
}
}
// return the object containing akte and dms data
return success(getData($akteResult));
}
/**
* Removes Akte by akte Id, person id and/or dms id
* Removes all associated dms entries and versions, and deletes all associated files
* Returns success with removed version numbers or error
*/
private function _remove($akte_id = null, $person_id = null, $dms_id = null)
{
// Get dms_id for akte
$this->_ci->AkteModel->addSelect('akte_id');
$this->_ci->AkteModel->addSelect('dms_id');
$akteResult = $this->_ci->AkteModel->load($akte_id);
$paramArray = array();
if (is_numeric($akte_id)) $paramArray['akte_id'] = $akte_id;
if (is_numeric($person_id)) $paramArray['person_id'] = $person_id;
if (is_numeric($dms_id)) $paramArray['dms_id'] = $dms_id;
$akteResult = $this->_ci->AkteModel->loadWhere($paramArray);
if (isError($akteResult)) return $akteResult;
if (!hasData($akteResult)) return error("Akte not found");
if (!hasData($akteResult)) return error('Akte not found');
$dms_id = getData($akteResult)[0]->dms_id;
$error = null;
// Delete Akte
$deleteResult = $this->_ci->AkteModel->delete(getData($akteResult)[0]->akte_id);
// Start DB transaction to avoid deleting only part of the data
$this->_ci->db->trans_begin();
if (isError($deleteResult)) return $deleteResult;
// delete Akte
$deleteResult = $this->_ci->AkteModel->delete($akte_id);
// Remove all dms entry for dms of the akte
$removeAllResult = $this->_ci->dmslib->removeAll(getData($akteResult)[0]->dms_id);
if (isError($deleteResult))
{
$error = $deleteResult;
}
else
{
// remove all dms entry for dms of the akte
$removeAllResult = $this->_ci->dmslib->removeAll($dms_id);
if (isError($removeAllResult))
$error = $removeAllResult;
}
// Transaction complete!
$this->_ci->db->trans_complete();
// Check if everything went ok during the transaction
if ($this->_ci->db->trans_status() === false || isset($error))
{
$this->_ci->db->trans_rollback();
// return occured error
if (isset($error))
return $error;
else
return error("Error occured when deleting, rolled back");
}
else
{
$this->_ci->db->trans_commit();
// return removed dms entry data
return $removeAllResult;
}
return $removeAllResult;
}
}
-563
View File
@@ -1,563 +0,0 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class CmsAdminLib
{
private $ci;
public function __construct()
{
$this->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);
}
/**
* @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($this->ci->p->t('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($this->ci->p->t('cms', 'keineVorlageVorhanden'));
$template = $templates[0];
$oeResult = $this->getDefaultOe();
if (isError($oeResult))
return $oeResult;
$oe = getData($oeResult);
$titel = $this->ci->p->t('cms', 'neuerEintrag');
$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' => '<?xml version="1.0" encoding="UTF-8" ?><content></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($this->ci->p->t('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($this->ci->p->t('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
'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
'reviewvon' => null,
'reviewamum' => null,
'gesperrt_uid' => null,
'insertamum' => $now,
'insertvon' => $uid,
'updateamum' => $now,
'updatevon' => $uid
]);
if (isError($insertResult))
return $insertResult;
return success(['version' => $newVersion]);
}
/**
* Writes the properties of one content version.
* @param array $daten content_id, sprache, version, template_kurzbz, oe_kurzbz,
* aktiv, menu_open, beschreibung, titel, sichtbar
* @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
]);
$this->ci->ContentspracheModel->update($row->contentsprache_id, [
'titel' => $daten['titel'],
'sichtbar' => $daten['sichtbar'],
'updateamum' => $now,
'updatevon' => $uid
]);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'speichernFehlgeschlagen'));
return success(true);
}
// legacy cms checks neither the permission
// type nor the lock. The form appears only when the user holds the lock, but the POST
// itself is unprotected.
/**
* @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($this->ci->p->t('cms', 'contentNichtGefunden'));
$templateResult = $this->ci->TemplateModel->load($content[0]->template_kurzbz);
if (isError($templateResult))
return $templateResult;
$template = getData($templateResult);
if (empty($template))
return error($this->ci->p->t('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);
}
/**
* Locks a version for the current user, or takes over an expired lock.
* @param int $contentsprache_id
* @return object success(true) or error
*/
public function lock($contentsprache_id)
{
$uid = getAuthUID();
$rowResult = $this->ci->ContentspracheModel->load($contentsprache_id);
if (isError($rowResult))
return $rowResult;
$rowData = getData($rowResult);
if (empty($rowData))
return error($this->ci->p->t('cms', 'versionNichtGefunden'));
$holder = ($rowData[0]->gesperrt_uid === '') ? null : $rowData[0]->gesperrt_uid;
if ($holder !== null && $holder !== $uid)
{
$openResult = $this->ci->ContentlogModel->getOpenLock($contentsprache_id, $holder);
$openEntry = (!isError($openResult)) ? getData($openResult) : null;
if (!$this->isLockExpired($holder, $openEntry))
return error($this->ci->p->t('cms', 'bereitsGesperrt'));
}
$this->ci->db->trans_start();
$this->ci->ContentlogModel->closeOpenEntries($contentsprache_id);
$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($this->ci->p->t('cms', 'speichernFehlgeschlagen'));
return success(true);
}
// legacy released ALL locks of the user. Releasing now ends one lock.
// The uid predicate stops a user releasing a lock of somebody else.
/**
* Releases the lock of the current user on one version.
* @param int $contentsprache_id
* @return object success(true)
*/
public function releaseOwnLock($contentsprache_id)
{
$uid = getAuthUID();
$this->ci->db->trans_start();
$this->ci->ContentlogModel->closeOpenEntries($contentsprache_id, $uid);
$this->ci->ContentspracheModel->releaseLock($contentsprache_id, $uid);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('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->ContentlogModel->closeOpenEntries($contentsprache_id);
$this->ci->ContentspracheModel->releaseLock($contentsprache_id);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'speichernFehlgeschlagen'));
return success(true);
}
/**
* @param int $content_id
* @param string $sprache
* @param int $version
* @return object success(array) gesperrt_uid, start, expires, own, expired, may_force
*/
public function getLockState($content_id, $sprache, $version)
{
$versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
if (isError($versionResult))
return $versionResult;
$row = getData($versionResult);
// legacy admin.php inserts an empty string, releasing writes NULL.
// this code reports NULL.
$gesperrt_uid = ($row->gesperrt_uid === '') ? null : $row->gesperrt_uid;
$logEntry = null;
if ($gesperrt_uid !== null)
{
$lockResult = $this->ci->ContentlogModel->getOpenLock(
$row->contentsprache_id, $gesperrt_uid
);
$logEntry = (!isError($lockResult)) ? getData($lockResult) : null;
}
return success([
'contentsprache_id' => (int) $row->contentsprache_id,
'gesperrt_uid' => $gesperrt_uid,
'start' => $logEntry ? $logEntry->start : null,
'own' => ($gesperrt_uid !== null && $gesperrt_uid === getAuthUID()),
'expired' => $this->isLockExpired($gesperrt_uid, $logEntry),
'expires' => $this->lockExpiryDate($logEntry),
'may_force' => $this->ci->permissionlib->isBerechtigt('basis/cms_sperrfreigabe', 'su')
]);
}
/**
* End of the lock window, null if the entry carries no start.
* @param stdClass|null $logEntry
* @return string|null
*/
private function lockExpiryDate($logEntry)
{
if ($logEntry === null || empty($logEntry->start))
return null;
$ttlHours = (int) $this->ci->config->item('lock_ttl_hours');
// lock() writes start with PHP time, so stay on PHP time.
return date('Y-m-d H:i:s', strtotime($logEntry->start) + $ttlHours * 3600);
}
// Locks now age out after lock_ttl_hours.
/**
* A lock without a log entry has no age and counts as expired.
* @param string|null $gesperrt_uid
* @param stdClass|null $logEntry
* @return bool
*/
private function isLockExpired($gesperrt_uid, $logEntry)
{
if ($gesperrt_uid === null)
return false;
$expires = $this->lockExpiryDate($logEntry);
return $expires === null || strtotime($expires) < time();
}
/**
* @param int $content_id
* @return object success(true) or error
*/
public function deleteContent($content_id)
{
$this->ci->db->trans_start();
$this->ci->ContentchildModel->deleteByContent($content_id);
$this->ci->ContentlogModel->deleteByContent($content_id);
$this->ci->ContentspracheModel->deleteByContent($content_id);
$this->ci->ContentgruppeModel->deleteByContent($content_id);
$this->ci->ContentModel->delete($content_id);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('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($this->ci->p->t('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->ContentlogModel->deleteByContentsprache($row->contentsprache_id);
$this->ci->ContentspracheModel->deleteVersion($content_id, $sprache, $version);
$this->ci->db->trans_complete();
if ($this->ci->db->trans_status() === false)
return error($this->ci->p->t('cms', 'loeschenFehlgeschlagen'));
return success(true);
}
}
+2 -4
View File
@@ -42,12 +42,10 @@ class CmsLib
* @param int $version
* @param string $sprache
* @param boolean $sichtbar
* @param boolean $log false suppresses the click log entry. The CMS admin
* preview renders a content without counting a view.
*
* @return stdClass
*/
public function getContent($content_id, $version = null, $sprache = null, $sichtbar = true, $log = true)
public function getContent($content_id, $version = null, $sprache = null, $sichtbar = true)
{
if (!is_numeric($content_id))
return error('ContentID ist ungueltig');
@@ -74,7 +72,7 @@ class CmsLib
return $content;
// Legt einen Logeintrag für die Klickstatistik an
if ($log && defined('LOG_CONTENT') && LOG_CONTENT) {
if (defined('LOG_CONTENT') && LOG_CONTENT) {
// Nur eingeloggte User werden geloggt, das sonst auch alle Infoscreenaufrufe und dgl. mitgeloggt werden
if (isLogged()) {
$request_data = 'content_id=' . $content_id;
+118 -523
View File
@@ -1,12 +1,21 @@
<?php
/**
* FH-Complete
/* Copyright (C) 2025 fhcomplete.net
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
*
* @package FHC-Helper
* @author FHC-Team
* @copyright Copyright (c) 2022 fhcomplete.net
* @license GPLv3
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
@@ -15,24 +24,17 @@ use \stdClass as stdClass;
class DmsLib
{
const FILE_CONTENT_PROPERTY = 'file_content'; // property name for file content
const DMS_SYS_NAME = 'DMS System';
private $_ci; // code igniter instance
private $_who; // who added this document
/**
* Object initialization
*/
public function __construct($params = null)
public function __construct()
{
$this->_ci =& get_instance();
// Set the the _who property
$this->_who = 'DMS system'; // default
// It is possible to set it using the who parameter
if (!isEmptyArray($params) && isset($params['who']) && !isEmptyString($params['who'])) $this->_who = $params['who'];
$this->_ci->load->model('crm/Akte_model', 'AkteModel'); // deprecated, should not be used here!
$this->_ci->load->model('content/Dms_model', 'DmsModel');
$this->_ci->load->model('content/DmsVersion_model', 'DmsVersionModel');
$this->_ci->load->model('content/DmsFS_model', 'DmsFSModel');
@@ -46,69 +48,85 @@ class DmsLib
* Returns success info of added dms entry (dms_id, version, filename) or error
*/
public function add(
$name, $mimetype, $fileHandle, // Required parameters
$kategorie_kurzbz = null, $dokument_kurzbz = null, $beschreibung = null, $cis_suche = false, $schlagworte = null
// Required parameters
$name, $mimetype, $fileHandle,
// Optional parameters
$kategorie_kurzbz = null, $dokument_kurzbz = null, $beschreibung = null, $cis_suche = false, $schlagworte = null, $insertvon = self::DMS_SYS_NAME
)
{
// create unique filename, using original document name to detect file extension
$filename = $this->_getUniqueFilename($name);
// If the file handle is not valid
if ($fileHandle === false) return error('Was not possible to open the given file');
// copy file from fileHandle to dms folder
$copyFileResult = $this->_copyFile($fileHandle, $filename);
// Create unique filename, using original document name to detect file extension
$filename = $this->_getUniqueFilename($name);
if (isError($copyFileResult)) return $copyFileResult;
// Copy file from fileHandle to dms folder
$copyFileResult = $this->_copyFile($fileHandle, $filename);
// if file written successful, insert dms
$dmsResult = $this->_ci->DmsModel->insert(
array(
'kategorie_kurzbz' => $kategorie_kurzbz,
'dokument_kurzbz' => $dokument_kurzbz
)
// If an error occrured while copying the file
if (isError($copyFileResult)) return $copyFileResult;
// Insert the new DMS
$dmsResult = $this->_ci->DmsModel->insert(
array(
'kategorie_kurzbz' => $kategorie_kurzbz,
'dokument_kurzbz' => $dokument_kurzbz
)
);
// If an error occurred
if (isError($dmsResult)) return $dmsResult;
// If a DMS in the previous insert returned the new PK value
if (hasData($dmsResult))
{
$dms_id = getData($dmsResult);
// Insert the DMS version
$dmsVersion = array(
'dms_id' => $dms_id,
'version' => 0,
'filename' => $filename,
'mimetype' => $mimetype,
'name' => $name,
'beschreibung' => $beschreibung,
'cis_suche' => $cis_suche,
'schlagworte' => $schlagworte,
'insertvon' => $insertvon,
'insertamum' => date('Y-m-d H:i:s')
);
if (isError($dmsResult)) return $dmsResult;
$dmsVersionResult = $this->_ci->DmsVersionModel->insert($dmsVersion);
if (hasData($dmsResult))
{
$dms_id = getData($dmsResult);
$version = 0;
// If an error occured
if (isError($dmsVersionResult)) return $dmsVersionResult;
// insert dms version
$dmsVersion = array(
'dms_id' => $dms_id,
'version' => $version,
'filename' => $filename,
'mimetype' => $mimetype,
'name' => $name,
'beschreibung' => $beschreibung,
'cis_suche' => $cis_suche,
'schlagworte' => $schlagworte,
'insertvon' => $this->_who,
'insertamum' => date('Y-m-d H:i:s')
);
// Return dms info
$resObj = new stdClass();
$resObj->dms_id = $dms_id;
$resObj->version = 0;
$resObj->filename = $filename;
$dmsVersionResult = $this->_ci->DmsVersionModel->insert($dmsVersion);
if (isError($dmsVersionResult)) return $dmsVersionResult;
// return dms info
$resObj = new stdClass();
$resObj->dms_id = $dms_id;
$resObj->version = $version;
$resObj->filename = $filename;
return success($resObj);
}
else
return error("error when inserting DMS");
return success($resObj);
}
else
return success();
}
/**
* Writes a new file with content of fileHandle, adds a new dms version (max version number + 1) for the written file
* Returns success with info of added dms version (version, filename) or error
*/
public function addNewVersion($dms_id, $fileHandle, $name = null, $mimetype = null, $beschreibung = null, $cis_suche = false, $schlagworte = null)
public function addNewVersion(
// Required parameters
$dms_id, $fileHandle,
// Optional parameters
$name = null, $mimetype = null, $beschreibung = null, $cis_suche = false, $schlagworte = null, $insertvon = self::DMS_SYS_NAME
)
{
// If the file handle is not valid
if ($fileHandle === false) return error('Was not possible to open the given file');
// get the latest version
$lastVersionResult = $this->getLastVersion($dms_id);
@@ -141,7 +159,7 @@ class DmsLib
'beschreibung' => isset($beschreibung) ? $beschreibung : $lastVersion->beschreibung,
'cis_suche' => isset($cis_suche) ? $cis_suche : $lastVersion->cis_suche,
'schlagworte' => isset($schlagworte) ? $schlagworte : $lastVersion->schlagworte,
'insertvon' => $this->_who,
'insertvon' => $insertvon,
'insertamum' => date('Y-m-d H:i:s')
);
@@ -157,7 +175,7 @@ class DmsLib
return success($resObj);
}
else
return error("last version not found");
return success();
}
/**
@@ -167,6 +185,9 @@ class DmsLib
*/
public function updateLastVersion($dms_id, $fileHandle, $name = null, $mimetype = null, $beschreibung = null, $cis_suche = false, $schlagworte = null)
{
// If the file handle is not valid
if ($fileHandle === false) return error('Was not possible to open the given file');
// get the latest version
$lastVersionResult = $this->getLastVersion($dms_id);
@@ -208,12 +229,12 @@ class DmsLib
return success($resObj);
}
else
return error("last version not found");
return success();
}
/**
* Gets dms version with highest number
* Returns success with dms data and fileHandle with file content or error
* Returns success with dms data or error
*/
public function getLastVersion($dms_id)
{
@@ -237,12 +258,12 @@ class DmsLib
return $this->getVersion($dms_id, $lastDmsVersion);
}
else
return error("Dms last version not found");
return success();
}
/**
* Gets specified dms version
* Returns success with dms data and fileHandle with file content or error
* Returns success with dms data or error
*/
public function getVersion($dms_id, $version)
{
@@ -254,34 +275,7 @@ class DmsLib
)
);
if (isError($dmsVersionResult)) return $dmsVersionResult;
if (hasData($dmsVersionResult))
{
$dmsVersion = getData($dmsVersionResult)[0];
// get file content as file pointer
$fileHandleResult = $this->_ci->DmsFSModel->openRead($dmsVersion->filename);
if (isError($fileHandleResult)) return $fileHandleResult;
if (hasData($fileHandleResult))
{
$fileHandle = getData($fileHandleResult);
$dmsVersion->{self::FILE_CONTENT_PROPERTY} = $fileHandle;
// close file pointer
$closeResult = $this->_ci->DmsFSModel->close($fileHandle);
if (isError($closeResult)) return $closeResult;
return success($dmsVersion);
}
else
return error("File could not be opened");
}
else
return error("Dms version not found");
return $dmsVersionResult;
}
/**
@@ -420,6 +414,34 @@ class DmsLib
return success($removeVersionResultObj);
}
/**
* Get info from the DMS to be provided to the FHC_Controller->outputFile
*/
public function getOutputFileInfo($dms_id, $file_name = '', $disposition = 'attachment')
{
// Loads the last DMS version from database
$lastVersionResult = $this->getLastVersion($dms_id);
// If an error occurred then return it
if (isError($lastVersionResult)) return $lastVersionResult;
// If has been found
if (hasData($lastVersionResult))
{
$lastVersion = getData($lastVersionResult)[0];
$fileObj = new stdClass();
$fileObj->file = DMS_PATH.$lastVersion->filename;
$fileObj->name = isEmptyString($file_name) ? $lastVersion->name : $file_name;
$fileObj->mimetype = $lastVersion->mimetype;
$fileObj->disposition = $disposition;
return success($fileObj);
}
return success();
}
// -----------------------------------------------------------------------------------------------------------
// Private methods
@@ -474,432 +496,5 @@ class DmsLib
return $uniqueFilename;
}
// -----------------------------------------------------------------------------------------------------------
// Deprecated methods, not to be used
/**
* Load a DMS Document.
* If no version is particularly given, the latest version is loaded.
*
* @param $dms_id
* @param integer $version
* @return array
*/
public function load($dms_id, $version = null)
{
if (is_numeric($dms_id))
{
$this->_ci->DmsModel->addJoin('campus.tbl_dms_version', 'dms_id');
$this->_ci->DmsModel->addOrder('version', 'DESC');
$this->_ci->DmsModel->addLimit(1);
if (!is_numeric($version))
{
return $this->_ci->DmsModel->load($dms_id);
}
else
{
return $this->_ci->DmsModel->loadWhere(array('dms_id' => $dms_id, 'version' => $version));
}
}
return error('The parameter DMS ID must be a number');
}
/**
* Read a DMS Document from the Filesystem
* @param int $dms_id ID of the Document.
* @param int $version The version of the Document (latest if null).
* @return object success or error
*/
public function read($dms_id, $version = null)
{
$result = error('Wrong dms_id parameter');
if (isset($dms_id))
{
$this->_ci->DmsModel->addJoin('campus.tbl_dms_version', 'dms_id');
$this->_ci->DmsModel->addOrder('version', 'DESC');
$this->_ci->DmsModel->addLimit(1);
if (!isset($version))
{
$result = $this->_ci->DmsModel->load($dms_id);
}
else
{
$result = $this->_ci->DmsModel->loadWhere(array('dms_id' => $dms_id, 'version' => $version));
}
// If a dms has been found
if (hasData($result))
{
$resultFS = $this->_ci->DmsFSModel->readBase64(getData($result)[0]->filename);
if (isError($resultFS)) return $resultFS; // if an error occurred return it
$result->retval[0]->{self::FILE_CONTENT_PROPERTY} = getData($resultFS);
}
}
return $result;
}
/**
* Get all accepted Documents of a Person
*
* @param int $person_id ID of the person.
* @param string $dokument_kurzbz Type of document.
* @param bool $no_file If null then loads also the content.
* @return object success or error
*/
public function getAktenAcceptedDms($person_id, $dokument_kurzbz = null, $no_file = null)
{
$result = $this->_ci->AkteModel->getAktenAcceptedDms($person_id, $dokument_kurzbz);
if (hasData($result) && $no_file == null)
{
for ($i = 0; $i < count(getData($result)); $i++)
{
$resultFS = $this->_ci->DmsFSModel->readBase64(getData($result)[$i]->filename);
if (isError($resultFS)) return $resultFS; // if an error occurred return it
$result->retval[$i]->{self::FILE_CONTENT_PROPERTY} = getData($resultFS);
}
}
return $result;
}
/**
* Uploads a document and saves it to DMS
* @param $dms DMS assoc array
* @param $field_name Name of the HTML uploadfile input name attribute
* @param array $allowed_types Default: all. Param example: array(jpg, pdf)
* @return array
*/
public function upload($dms, $field_name, $allowed_types = array('*'))
{
// Init upload configs
$this->_loadUploadLibrary($allowed_types);
if (!$this->_ci->upload->do_upload($field_name))
{
return error($this->_ci->upload->display_errors());
}
$upload_data = $this->_ci->upload->data(); // data about the uploaded file
// Insert to DMS table
$insDmsResult = $this->_ci->DmsModel->insert($this->_ci->DmsModel->filterFields($dms));
if (isError($insDmsResult)) return $insDmsResult;
$upload_data['dms_id'] = getData($insDmsResult);
if(isset($upload_data['file_type']) && !isset($dms['mimetype']))
$dms['mimetype'] = $upload_data['file_type'];
// Insert DMS version
$insVersionResult = $this->_ci->DmsVersionModel->insert(
$this->_ci->DmsVersionModel->filterFields(
$dms,
$upload_data['dms_id'],
$upload_data['file_name']
)
);
if (isError($insVersionResult)) return $insVersionResult;
// Return result of uploaded data
return success($upload_data);
}
/**
* Download a document.
*
* @param $dms_id
* @param string $filename $filename If String is given, it will be used as filename on download
* @param string $disposition [inline | attachment]
* Inline opens doc in new tab. Attachment displays download dialog box.
*/
public function download($dms_id, $filename = null, $disposition = 'inline')
{
// Retrieves info about the given dms
$fileInfoResult = $this->getFileInfo($dms_id);
if (isError($fileInfoResult)) return error(getError($fileInfoResult));
// If data have been found
if (hasData($fileInfoResult))
{
$fileObj = getData($fileInfoResult);
// Change filename, if filename is provided
if (!isEmptyString($filename)) $fileObj->name = $filename;
// Add file disposition if disposition has a valid value
if ($disposition == 'attachment' || $disposition == 'inline')
{
$fileObj->disposition = $disposition;
}
return success($fileObj);
}
// If no data have been found then return an empty success
return success();
}
/**
* Get file information.
*
* @param $dms_id
* @param integer $version
* @return array with File Object.
*/
public function getFileInfo($dms_id, $version = null)
{
// Checks the dms_id parameter
if (!is_numeric($dms_id)) return error('Wrong parameter');
// Load DMS from database
$result = $this->load($dms_id, $version);
if (isError($result)) return error(getError($result));
// If data have been found
if (hasData($result))
{
// Store file information in fileObj
$fileObj = new stdClass();
$fileObj->filename = getData($result)[0]->filename;
$fileObj->file = DMS_PATH.getData($result)[0]->filename;
$fileObj->name = getData($result)[0]->name; // original user filename
$fileObj->mimetype = getData($result)[0]->mimetype;
return success($fileObj);
}
// If no data have been found return an empty success
return success();
}
/**
* Saves a Document
* @param object $dms DMS Object ot be saved.
* @return object
*/
public function save($dms)
{
$result = null;
if (isset($dms['new']) && $dms['new'] == true)
{
// Remove new parameter to avoid DB insert errors
unset($dms['new']);
$result = $this->_saveFileOnInsert($dms);
if (isSuccess($result))
{
$filename = getData($result);
if (isset($dms['dms_id']) && $dms['dms_id'] != '')
{
$result = $this->_ci->DmsVersionModel->insert(
$this->_ci->DmsVersionModel->filterFields($dms, $dms['dms_id'], $filename)
);
}
else
{
$result = $this->_ci->DmsModel->insert($this->_ci->DmsModel->filterFields($dms));
if (isSuccess($result))
{
$result = $this->_ci->DmsVersionModel->insert(
$this->_ci->DmsVersionModel->filterFields($dms, getData($result), $filename)
);
}
}
}
}
else
{
$result = $this->_saveFileOnUpdate($dms);
if (isSuccess($result))
{
$result = $this->_ci->DmsModel->update($dms['dms_id'], $this->_ci->DmsModel->filterFields($dms));
if (isSuccess($result))
{
$result = $this->_ci->DmsVersionModel->update(
array(
$dms['dms_id'],
$dms['version']
),
$this->_ci->DmsVersionModel->filterFields($dms)
);
}
}
}
return $result;
}
/**
* Deletes a Akte of a Person
* @param int $person_id ID of the person.
* @param int $dms_id Id of the Document.
* @return object
*/
public function delete($person_id, $dms_id)
{
$result = null;
// If the parameters are valid
if (is_numeric($person_id) && is_numeric($dms_id))
{
// Start DB transaction
$this->_ci->db->trans_start(false);
// Get akte_id from table tbl_akte
$result = $this->_ci->AkteModel->loadWhere(array('person_id' => $person_id, 'dms_id' => $dms_id));
if (isSuccess($result))
{
// Delete all entries in tbl_akte
for ($i = 0; $i < count(getData($result)); $i++)
{
$this->_ci->AkteModel->delete(getData($result)[$i]->akte_id);
}
// Get all filenames related to this dms
$resultFileNames = $this->_ci->DmsVersionModel->loadWhere(array('dms_id' => $dms_id));
if (isSuccess($resultFileNames))
{
// Delete from tbl_dms_version
$result = $this->_ci->DmsVersionModel->delete(array('dms_id' => $dms_id));
if (isSuccess($result))
{
// Delete from tbl_dms
$result = $this->_ci->DmsModel->delete($dms_id);
}
}
}
// Transaction complete!
$this->_ci->db->trans_complete();
// Check if everything went ok during the transaction
if ($this->_ci->db->trans_status() === false || isError($result))
{
$this->_ci->db->trans_rollback();
$result = error('An error occurred while performing a delete operation', EXIT_ERROR);
}
else
{
$this->_ci->db->trans_commit();
$result = success('Dms successfully removed from DB');
}
// If everything is ok
if (isSuccess($result))
{
// Remove all files related to this person and dms
for ($i = 0; $i < count(getData($resultFileNames)); $i++)
{
$this->_ci->DmsFSModel->removeBase64(getData($resultFileNames)[$i]->filename);
}
}
}
else
{
$result = error('Invalid parameters');
}
return $result;
}
/**
* Loads the Content of an akte
* @param int $akte_id Id of the akte.
* @return object with document content or error
*/
public function getAkteContent($akte_id)
{
$akte = $this->_ci->AkteModel->load($akte_id);
if (hasData($akte))
{
if (getData($akte)[0]->inhalt != '')
{
return success(base64_decode(getData($akte)[0]->inhalt));
}
elseif (getData($akte)[0]->dms_id != '')
{
$dmscontent = $this->read(getData($akte)[0]->dms_id);
if (isSuccess($dmscontent))
{
return success(base64_decode(getData($dmscontent)[0]->file_content));
}
else
{
return error(getError($dmscontent));
}
}
else
{
return error('No Content available');
}
}
else
{
return error(getError($akte));
}
}
/**
* Saves the Content of a DMS in the Filesystem
* @param object $dms DMS object to be saved.
* @return object
*/
private function _saveFileOnInsert($dms)
{
$filename = uniqid().'.'.pathinfo($dms['name'], PATHINFO_EXTENSION);
$result = $this->_ci->DmsFSModel->writeBase64($filename, $dms['file_content']);
if (isSuccess($result))
{
$result = success($filename);
}
return $result;
}
/**
* Updates the File in the Filesystem
* @param object $dms DMS object to update.
* @return object
*/
private function _saveFileOnUpdate($dms)
{
$result = null;
if (isset($dms['version']))
{
$result = $this->read($dms['dms_id'], $dms['version']);
if (hasData($result))
{
$result = $this->_ci->DmsFSModel->writeBase64(getData($result)[0]->filename, $dms['file_content']);
}
}
return $result;
}
/**
* Loads the upload library of CI
*/
private function _loadUploadLibrary($allowed_types)
{
$config = array();
$config['upload_path'] = DMS_PATH;
$config['allowed_types'] = implode('|', $allowed_types);
$config['overwrite'] = true;
$config['file_name'] = uniqid().'.pdf';
$this->_ci->load->library('upload', $config);
$this->_ci->upload->initialize($config);
}
}
+37 -236
View File
@@ -1,5 +1,5 @@
<?php
/* Copyright (C) 2024 fhcomplete.net
/* Copyright (C) 2025 fhcomplete.net
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
@@ -34,15 +34,11 @@ use SimpleXMLElement as SimpleXMLElement;
* modifiing said data (sign()/setXMLTag_archivierbar()) or adding
* images (addImage()) and then call output() and close().
* Now the create, output and close functions are combined into one function and adding data and images is done via parameters.
* There are now two functions getContent() and showContent() where showContent() equals to output(false) and getContent equals to output(true)
* Instead of calling addDataArray, addDataXML or addDataURL just call
* getDataArray, getDataXML or getDataURL respectevily and use the return
* value as $xml_data parameter in the showContent and getContent calls.
* value as $xml_data parameter in the getContent call.
* Instead of calling addImages just create an array and pass it as $images
* parameter to the showContent/getContent function.
* The old setFilename() function is now a parameter in showContent(). It is
* not needed in getContent() since that function does not do anything that
* requires a filename.
* parameter to the getContent function.
* To get/show a signed document just pass a valid uid as $sign_user
* parameter.
*
@@ -63,21 +59,10 @@ use SimpleXMLElement as SimpleXMLElement;
* 'name' => $imagename,
* 'contenttype' => $imagecontenttype
* ]];
* $this->documentexportlib->showContent(
* $filename,
* $vorlage,
* $xml_data,
* $oe_kurzbz,
* $version,
* $outputformat,
* null,
* null,
* $images
* );
*/
class DocumentExportLib
{
private $unoconv_version;
private $_ci;
/**
* Constructor
@@ -85,33 +70,14 @@ class DocumentExportLib
public function __construct()
{
// Gets CI instance
$this->ci =& get_instance();
$this->_ci =& get_instance();
// Load Phrases
$this->ci->load->library('PhrasesLib', ['document_export', null], 'documentExportPhrases');
// Load phrases
$this->_ci->load->library('PhrasesLib', ['document_export', null], 'DocumentExportPhrases');
// Which document converter has to be used
if (defined('DOCSBOX_ENABLED') && DOCSBOX_ENABLED === true)
{
// Use docsbox!!
}
else
{
exec('unoconv --version', $ret_arr);
if(isset($ret_arr[0]))
{
$hlp = explode(' ', $ret_arr[0]);
if(isset($hlp[1]))
{
$this->unoconv_version = $hlp[1];
}
else
show_error($this->ci->documentExportPhrases->t("document_export", "error_unoconv_version"));
}
else
show_error($this->ci->documentExportPhrases->t("document_export", "error_unoconv"));
}
// Load libraries
$this->_ci->load->library('DocumentLib');
$this->_ci->load->library('SignatureLib');
}
/**
@@ -172,7 +138,7 @@ class DocumentExportLib
$xml_data = new DOMDocument;
if (!$xml_data->load($xml_url))
return error($this->ci->documentExportPhrases->t("document_export", "error_xml_load", [
return error($this->_ci->DocumentExportPhrases->t("document_export", "error_xml_load", [
"url" => $xml_url,
"xml" => $xml,
"params" => $params
@@ -236,7 +202,7 @@ class DocumentExportLib
$outputformat = $this->getDefaultOutputFormat($outputformat, $vorlage->mimetype);
$result = $this->createAndSignContent(
$createResult = $this->createAndSignContent(
$temp_folder,
$outputformat,
$vorlage,
@@ -247,118 +213,26 @@ class DocumentExportLib
$sign_user,
$sign_profile
);
if (isError($result)) {
if (isError($createResult)) {
$this->close($temp_folder, $source_folder);
return $result;
return $createResult;
}
$temp_filename = getData($result);
$temp_filename = getData($createResult);
$fsize = filesize($temp_filename);
$handle = fopen($temp_filename, 'r');
if (!$handle)
return error($this->ci->documentExportPhrases->t("document_export", "error_file_load"));
$result = fread($handle, $fsize);
return error($this->_ci->DocumentExportPhrases->t("document_export", "error_file_load"));
$fileContentResult = fread($handle, $fsize);
fclose($handle);
$this->close($temp_folder, $source_folder);
return success($result);
return success($fileContentResult);
}
/**
* Sets the headers and displays the Document.
* On failure the exit() function will be called
*
* @param string $filename
* @param stdClass $vorlage A db entry from tbl_vorlage
* @param DomDocument $xml_data
* @param string $oe_kurzbz
* @param integer|null $version (optional)
* @param string $outputformat (optional)
* @param string $sign_user (optional) Must be a valid uid
* @param string $sign_profile (optional) Signatureprofile for signing
* @param array $images (optional) Each element should have a property path, name & contenttype which are all strings
*
* @return void
*/
public function showContent(
$filename,
$vorlage,
$xml_data,
$oe_kurzbz,
$version = null,
$outputformat = null,
$sign_user = null,
$sign_profile = null,
$images = []
) {
$source_folder = getcwd();
$temp_folder = sys_get_temp_dir() . '/fhcunoconv-' . uniqid();
$outputformat = $this->getDefaultOutputFormat($outputformat, $vorlage->mimetype);
$result = $this->createAndSignContent(
$temp_folder,
$outputformat,
$vorlage,
$oe_kurzbz,
$version,
$xml_data,
$images,
$sign_user,
$sign_profile
);
if (isError($result)) {
$this->close($temp_folder, $source_folder);
exit(getError($result));
}
$temp_filename = getData($result);
$fsize = filesize($temp_filename);
$handle = fopen($temp_filename, 'r');
if (!$handle) {
$this->close($temp_folder, $source_folder);
exit($this->ci->documentExportPhrases->t("document_export", "error_file_load"));
}
if (headers_sent()) {
$this->close($temp_folder, $source_folder);
exit($this->ci->documentExportPhrases->t("document_export", "error_headers"));
}
switch ($outputformat) {
case 'pdf':
header('Content-type: application/pdf');
header('Content-Disposition: attachment; filename="' . $filename . '.pdf"');
header('Content-Length: ' . $fsize);
break;
case 'doc':
header('Content-type: application/vnd.ms-word');
header('Content-Disposition: attachment; filename="' . $filename . '.doc"');
header('Content-Length: ' . $fsize);
break;
case 'odt':
header('Content-type: application/vnd.oasis.opendocument.text');
header('Content-Disposition: attachment; filename="' . $filename . '.odt"');
header('Content-Length: ' . $fsize);
break;
default:
$this->close($temp_folder, $source_folder);
exit($this->ci->documentExportPhrases->t("document_export", "error_outputformat_missing"));
}
while (!feof($handle)) {
echo fread($handle, 8192);
}
fclose($handle);
$this->close($temp_folder, $source_folder);
}
/**
* Helper function for getContent and showContent.
* Helper function for getContent
* Creates the temp folder and calls create and sign functions.
*
* @param string $temp_folder
@@ -387,13 +261,13 @@ class DocumentExportLib
mkdir($temp_folder);
chdir($temp_folder);
$this->ci->load->model('system/Vorlagestudiengang_model', 'VorlagestudiengangModel');
$this->_ci->load->model('system/Vorlagestudiengang_model', 'VorlagestudiengangModel');
$result = $this->ci->VorlagestudiengangModel->getCurrent($vorlage->vorlage_kurzbz, $oe_kurzbz, $version);
$result = $this->_ci->VorlagestudiengangModel->getCurrent($vorlage->vorlage_kurzbz, $oe_kurzbz, $version);
if (isError($result))
return $result;
if (!hasData($result))
return error($this->ci->documentExportPhrases->t("document_export", "error_template_missing"));
return error($this->_ci->DocumentExportPhrases->t("document_export", "error_template_missing"));
$vorlage_stg = current(getData($result));
foreach ($vorlage_stg as $k => $v)
$vorlage->$k = $v;
@@ -437,7 +311,7 @@ class DocumentExportLib
{
$content_xsl = new DOMDocument();
if (!$content_xsl->loadXML($vorlage->text))
return error($this->ci->documentExportPhrases->t("document_export", "error_xsl_load"));
return error($this->_ci->DocumentExportPhrases->t("document_export", "error_xsl_load"));
$proc = new XSLTProcessor();
$proc->importStyleSheet($content_xsl);
@@ -454,7 +328,7 @@ class DocumentExportLib
if ($vorlage->style) {
$styles_xsl = new DOMDocument();
if (!$styles_xsl->loadXML($vorlage->style))
return error($this->ci->documentExportPhrases->t("document_export", "error_styles_load"));
return error($this->_ci->DocumentExportPhrases->t("document_export", "error_styles_load"));
$style_proc = new XSLTProcessor();
$style_proc->importStyleSheet($styles_xsl);
@@ -482,7 +356,7 @@ class DocumentExportLib
$tempname_zip = $temp_folder . '/out.zip';
if (!copy($zipfile, $tempname_zip))
return error($this->ci->documentExportPhrases->t("document_export", "error_file_copy"));
return error($this->_ci->DocumentExportPhrases->t("document_export", "error_file_copy"));
exec("zip $tempname_zip content.xml");
if (!is_null($styles_xsl))
@@ -502,7 +376,7 @@ class DocumentExportLib
$manifest_xml = new DOMDocument;
if (!$manifest_xml->loadXML($manifest))
return error($this->ci->documentExportPhrases->t("document_export", "error_manifest"));
return error($this->_ci->DocumentExportPhrases->t("document_export", "error_manifest"));
//root-node holen
$root = $manifest_xml->getElementsByTagName('manifest')->item(0);
@@ -529,42 +403,16 @@ class DocumentExportLib
clearstatcache();
$temp_filename = $temp_folder . '/out.' . $outputformat;
switch ($outputformat) {
case 'pdf':
case 'doc':
$ret = 0;
$temp_filename = $temp_folder . '/out.' . $outputformat;
$converResult = $this->_ci->documentlib->convert($tempname_zip, $temp_filename, $outputformat);
if (defined('DOCSBOX_ENABLED') && DOCSBOX_ENABLED === true) {
// Use docsbox
$this->ci->load->library("DocsboxLib");
$docboxlib = get_class($this->ci->docboxlib);
$ret = $docboxlib::convert($tempname_zip, $temp_filename, $outputformat);
} else {
// Use unoconv
// Unoconv Version 0.6 hat eine Bug wodurch die Berechtigungen des PDF/Doc nicht korrekt gesetzt
// werden. Deshalb wird dies hier speziell behandelt.
// Die 2. Variante hat den Vorteil dass hier eine bessere Fehlerbehandlung moeglich ist
if ($this->unoconv_version == '0.6')
$command = 'unoconv -e IsSkipEmptyPages=false -f ' . $outputformat . ' %2$s > %1$s';
else
$command = 'unoconv -e IsSkipEmptyPages=false -f ' . $outputformat . ' --output %s %s 2>&1';
$command = sprintf($command, $temp_filename, $tempname_zip);
exec($command, $out, $ret);
}
if ($ret)
return error($this->ci->documentExportPhrases->t("document_export", "error_conv_timeout"));
if (isError($converResult))
return error($this->_ci->DocumentExportPhrases->t('document_export', 'error_conv_timeout'));
break;
case 'odt':
default:
$temp_filename = $tempname_zip;
}
return success($temp_filename);
@@ -584,63 +432,15 @@ class DocumentExportLib
*/
protected function sign($temp_folder, $temp_filename, $outputformat, $user, $profile)
{
if ($outputformat != 'pdf')
return error($this->ci->documentExportPhrases->t("document_export", "error_sign_pdf"));
if ($outputformat != 'pdf') return error($this->_ci->DocumentExportPhrases->t('document_export', 'error_sign_pdf'));
// Load the File
$file_data = file_get_contents($temp_filename);
$signed_filename = $this->_ci->signaturelib->sign($temp_folder, $temp_filename, $user, $profile);
$data = new stdClass();
$data->document = base64_encode($file_data);
// If fine then return it
if (isSuccess($signed_filename)) return $signed_filename;
// Signatur Profil
if (!is_null($profile))
$data->profile = $profile;
else
$data->profile = SIGNATUR_DEFAULT_PROFILE;
// Username des Endusers der die Signatur angefordert hat
$data->user = $user;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, SIGNATUR_URL . '/' . SIGNATUR_SIGN_API);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 7);
curl_setopt($ch, CURLOPT_USERAGENT, "FH-Complete");
// SSL Zertifikatsprüfung deaktivieren
// Besser ist es das Zertifikat am Server zu installieren!
//curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
//curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$data_string = json_encode($data, JSON_FORCE_OBJECT);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length:' . mb_strlen($data_string),
'Authorization: Basic ' . base64_encode(SIGNATUR_USER . ":" . SIGNATUR_PASSWORD)
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
curl_close($ch);
return error($this->ci->documentExportPhrases->t("document_export", "error_sign_timeout"));
}
curl_close($ch);
$resultdata = json_decode($result);
// If it is success
if (isset($resultdata->error) && $resultdata->error == 0) {
$signed_filename = $temp_folder . '/signed.pdf';
file_put_contents($signed_filename, base64_decode($resultdata->retval));
return success($signed_filename);
}
// otherwise if it is an error
return error($resultdata->retval ?? $this->ci->documentExportPhrases->t("global", "unknown_error", ["error" => $result]));
// Otherwise it is an error
return error($this->_ci->DocumentExportPhrases->t('global', 'unknown_error', ['error' => getError($signed_filename)]));
}
/**
@@ -717,3 +517,4 @@ class DocumentExportLib
return 'pdf';
}
}
+25 -24
View File
@@ -4,7 +4,8 @@ if (! defined('BASEPATH')) exit('No direct script access allowed');
class DocumentLib
{
private $unoconv_version;
private $_ci;
private $_unoconv_version;
/**
* Constructor
@@ -12,7 +13,10 @@ class DocumentLib
public function __construct()
{
// Gets CI instance
$this->ci =& get_instance();
$this->_ci =& get_instance();
// Load phrases
$this->_ci->load->library('PhrasesLib', ['document_export', null], 'DocumentExportPhrases');
// Which document converter has to be used
if (defined('DOCSBOX_ENABLED') && DOCSBOX_ENABLED === true)
@@ -28,13 +32,13 @@ class DocumentLib
$hlp = explode(' ', $ret_arr[0]);
if(isset($hlp[1]))
{
$this->unoconv_version = $hlp[1];
$this->_unoconv_version = $hlp[1];
}
else
show_error('Could not get Unoconv Version');
show_error($this->_ci->DocumentExportPhrases->t('document_export', 'error_unoconv_version'));
}
else
show_error('Unoconv not found - Please install Unoconv');
show_error($this->_ci->DocumentExportPhrases->t('document_export', 'error_unoconv'));
}
}
@@ -56,7 +60,7 @@ class DocumentLib
case 'image/jpeg':
case 'image/jpg':
case 'image/pjpeg':
$this->_jpegtopdf($filename, $outFile);
$this->_jpegToPdf($filename, $outFile);
return success($outFile);
case 'application/vnd.oasis.opendocument.spreadsheet':
case 'application/msword':
@@ -72,23 +76,19 @@ class DocumentLib
else
{
// Unoconv Version 0.6 seems to fail on converting TXT Files
if ($this->unoconv_version == '0.6')
return error();
if ($this->_unoconv_version == '0.6') return error('Unoconv Version 0.6 does not convert TXT Files correctly');
}
// Convertion
$ret = $this->convert($filename, $outFile, 'pdf');
if(isSuccess($ret))
{
return success($outFile);
}
else
{
return error(getError($ret));
}
if(isSuccess($ret)) return success($outFile);
// Error
return $ret;
case 'application/pdf':
return success($filename);
default:
return error('Unknown Mimetype:'.$mimetype);
return error('Unknown Mimetype:' . $mimetype);
}
}
@@ -101,20 +101,20 @@ class DocumentLib
*/
public function mergePDF($files, $outFile)
{
$cmd = "gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$outFile ";
$cmd = 'gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$outFile ';
$finfo = finfo_open(FILEINFO_MIME_TYPE);
// add all pdf files to the command
foreach ($files as $f)
{
$cmd .= $f." ";
$cmd .= $f.' ';
if (!file_exists($f))
{
return error("File not found: '$f'");
}
if (finfo_file($finfo, $f) != "application/pdf")
if (finfo_file($finfo, $f) != 'application/pdf')
{
return error("Wrong format(".finfo_file($finfo, $f)."): '$f'");
return error('Wrong format('.finfo_file($finfo, $f)."): '$f'");
}
}
@@ -144,13 +144,13 @@ class DocumentLib
// If it is set to use docsbox
if (defined('DOCSBOX_ENABLED') && DOCSBOX_ENABLED === true)
{
require_once(dirname(__FILE__).'/../application/libraries/DocsboxLib.php');
require_once(dirname(__FILE__).'/DocsboxLib.php');
$ret = DocsboxLib::convert($inFile, $outFile, $format);
}
else // otherwise use unoconv
{
if ($this->unoconv_version == '0.6')
if ($this->_unoconv_version == '0.6')
$command = 'unoconv -f %1$s %3$s > %2$s';
else
$command = 'unoconv -f %s --output %s %s 2>&1';
@@ -175,7 +175,7 @@ class DocumentLib
* @param string $outfile Path to Output (pdf) File.
* @return success or error object
*/
private function _jpegtopdf($filename, $outfile)
private function _jpegToPdf($filename, $outfile)
{
if (!file_exists($filename))
return error('File does not exists');
@@ -230,3 +230,4 @@ class DocumentLib
return true;
}
}
+77 -9
View File
@@ -36,15 +36,7 @@ class SignatureLib
try
{
// Dont send Document if it is bigger than 30 MB (Limit of Signature Server)
if (filesize($inputFileName) > 30000000)
{
$returnObject = new stdClass();
$returnObject->code = 1;
$returnObject->error = 1;
$returnObject->retval = 'File to big';
return $returnObject;
}
if (filesize($inputFileName) > 30000000) return $this->_returnObject(1, 1, 'File too big');
// Get the content of the given file
$inputFileContent = file_get_contents($inputFileName);
@@ -82,4 +74,80 @@ class SignatureLib
// Otherwise return a null as error
return null;
}
/**
*
*/
public function sign($temp_folder, $inputFileName, $user, $profile)
{
// Load the File
$file_data = file_get_contents($inputFileName);
$data = new stdClass();
$data->document = base64_encode($file_data);
// Signatur Profil
if (!is_null($profile))
$data->profile = $profile;
else
$data->profile = SIGNATUR_DEFAULT_PROFILE;
// Username des Endusers der die Signatur angefordert hat
$data->user = $user;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, SIGNATUR_URL . '/' . SIGNATUR_SIGN_API);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 7);
curl_setopt($ch, CURLOPT_USERAGENT, "FH-Complete");
// SSL Zertifikatsprüfung deaktivieren
// Besser ist es das Zertifikat am Server zu installieren!
//curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
//curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$data_string = json_encode($data, JSON_FORCE_OBJECT);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length:' . mb_strlen($data_string),
'Authorization: Basic ' . base64_encode(SIGNATUR_USER . ':' . SIGNATUR_PASSWORD)
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
curl_close($ch);
return $this->_returnObject(1, 1, 'CURL error');
}
curl_close($ch);
$resultdata = json_decode($result);
// If it is success
if (isset($resultdata->error) && $resultdata->error == 0)
{
$signed_filename = $temp_folder . '/signed.pdf';
file_put_contents($signed_filename, base64_decode($resultdata->retval));
return $this->_returnObject(0, 0, $signed_filename);
}
// Otherwise it is an error
return $this->_returnObject(1, 1, 'Error while signing the given document');
}
/**
*
*/
private function _returnObject($code, $error, $retval)
{
$returnObject = new stdClass();
$returnObject->code = $code;
$returnObject->error = $error;
$returnObject->retval = $retval;
return $returnObject;
}
}
-195
View File
@@ -1,195 +0,0 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
use \DOMDocument as DOMDocument;
class XsdSchemaLib
{
private $ci;
public function __construct()
{
$this->ci =& get_instance();
}
/**
* Converts an XSD into a field schema.
* @param string $xsd the XSD as a string
* @param string $templateKurzbz short name of the template
* @return object success(array) with template_kurzbz, root, fields
*/
public function parseSchema($xsd, $templateKurzbz)
{
libxml_use_internal_errors(true);
$dom = new DOMDocument();
if (!$dom->loadXML($xsd))
{
libxml_clear_errors();
return error($this->ci->p->t('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 version="1.0" encoding="UTF-8"?>';
$xml .= "\n<" . $schema['root'] . '>';
foreach ($schema['fields'] as $field)
{
$value = isset($values[$field['name']]) ? $values[$field['name']] : '';
$xml .= "\n<" . $field['name'] . '><![CDATA[' . $this->_escapeCdata($value) . ']]></' . $field['name'] . '>';
}
$xml .= "\n</" . $schema['root'] . '>';
libxml_use_internal_errors(true);
$check = new DOMDocument();
if (!$check->loadXML($xml))
{
libxml_clear_errors();
return error($this->ci->p->t('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(']]>', ']]]]><![CDATA[>', $value);
}
}
@@ -323,266 +323,4 @@ 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);
}
/**
* Per content metadata for the tree filter. Excludes news, like getRootContent().
* updateamum is the last touch on the content or on any of its versions.
*
* contentlength counts the visible characters of the newest version, the way getOne()
* picks it: the default language first, then the highest version. The XML scaffolding,
* the CDATA markers, the HTML tags, the entities and every space drop out, so a content
* that nobody ever filled measures 0 whatever empty shape it carries. The title counts
* too, so a content with a title and no body measures short instead of zero.
*
* @param string $uid current user, decides the mine flag
* @param string $sprache preferred language of the measured version
* @return stdClass success with array of rows or error
*/
public function getTreeMeta($uid, $sprache)
{
$query = "
SELECT tbl_content.content_id,
(SELECT COUNT(*) FROM campus.tbl_contentchild
WHERE tbl_contentchild.content_id = tbl_content.content_id) AS childcount,
tbl_content.insertamum,
GREATEST(
tbl_content.updateamum,
(SELECT MAX(updateamum) FROM campus.tbl_contentsprache
WHERE tbl_contentsprache.content_id = tbl_content.content_id)
) AS updateamum,
COALESCE(
tbl_content.insertvon = ? OR tbl_content.updatevon = ? OR EXISTS (
SELECT 1 FROM campus.tbl_contentsprache
WHERE tbl_contentsprache.content_id = tbl_content.content_id
AND (tbl_contentsprache.insertvon = ?
OR tbl_contentsprache.updatevon = ?)
),
false
) AS mine,
(SELECT length(
regexp_replace(
regexp_replace(
regexp_replace(
replace(replace(cs.content::text, '<![CDATA[', ''), ']]>', ''),
'<[^>]*>', '', 'g'),
'&[a-zA-Z]+;|&#[0-9]+;', '', 'g'),
'\s+', '', 'g')
)
FROM campus.tbl_contentsprache cs
WHERE cs.content_id = tbl_content.content_id
ORDER BY (cs.sprache = ?) DESC, cs.version DESC
LIMIT 1) AS contentlength
FROM campus.tbl_content
WHERE tbl_content.template_kurzbz <> ?
";
return $this->execReadOnlyQuery(
$query, [$uid, $uid, $uid, $uid, $sprache, 'news']
);
}
/**
* 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);
}
}
@@ -1,9 +1,6 @@
<?php
class Contentchild_model extends DB_Model
{
// swapSort reports the two boundary cases with this. See swapSort().
const NO_NEIGHBOUR = 'no neighbour';
/**
* Constructor
@@ -14,345 +11,4 @@ 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]);
}
/**
* Parents of a content, with the title of each parent.
* A content can hang under more than one parent, so this returns a list.
* @param int $content_id
* @param string $sprache
* @return stdClass success with array of rows or error
*/
public function getParents($content_id, $sprache)
{
$query = '
SELECT cc.contentchild_id, cc.content_id, cc.sort,
(SELECT titel FROM campus.tbl_contentsprache
WHERE content_id = cc.content_id AND sprache = ?
ORDER BY version LIMIT 1) AS titel
FROM campus.tbl_contentchild cc
WHERE cc.child_content_id = ?
ORDER BY cc.content_id
';
return $this->execReadOnlyQuery($query, [$sprache, $content_id]);
}
/**
* Children of a content as a reader may see them.
*
* Differs from getChilds() on purpose. getChilds() serves the admin and returns every
* child with the lowest version. A reader needs the newest visible version, only active
* contents, and no page that a group rule keeps from them.
*
* The group rule follows CmsLib::getContent(): a content without a row in
* tbl_contentgruppe is open, a content with rows needs membership in one of them.
*
* @param int $content_id
* @param string $sprache
* @param string $uid
* @return stdClass success with array of rows or error
*/
public function getChildsForReader($content_id, $sprache, $uid)
{
$query = '
SELECT
cc.child_content_id AS content_id,
cc.sort,
COALESCE(
(SELECT cs.titel FROM campus.tbl_contentsprache cs
WHERE cs.content_id = cc.child_content_id
AND cs.sprache = ?
AND cs.sichtbar
AND LENGTH(cs.titel) > 0
ORDER BY cs.version DESC NULLS LAST LIMIT 1),
(SELECT cs.titel FROM campus.tbl_contentsprache cs
WHERE cs.content_id = cc.child_content_id
AND cs.sichtbar
AND LENGTH(cs.titel) > 0
ORDER BY cs.version DESC NULLS LAST LIMIT 1),
c.beschreibung
) AS titel
FROM campus.tbl_contentchild cc
JOIN campus.tbl_content c ON c.content_id = cc.child_content_id
WHERE cc.content_id = ?
AND c.aktiv
-- Without a visible version the reader cannot open the page at all.
AND EXISTS (
SELECT 1 FROM campus.tbl_contentsprache cs
WHERE cs.content_id = cc.child_content_id AND cs.sichtbar
)
AND (
NOT EXISTS (
SELECT 1 FROM campus.tbl_contentgruppe cg
WHERE cg.content_id = cc.child_content_id
)
OR EXISTS (
SELECT 1 FROM campus.tbl_contentgruppe cg
JOIN public.vw_gruppen vg ON vg.gruppe_kurzbz = cg.gruppe_kurzbz
WHERE cg.content_id = cc.child_content_id AND vg.uid = ?
)
)
ORDER BY cc.sort, cc.contentchild_id
';
return $this->execReadOnlyQuery($query, [$sprache, $content_id, $uid]);
}
/**
* 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);
// A marker, not a message. The controller owns the wording, because a model has no
// phrases. Returning a phrase key here put the key itself in front of the user.
if (empty($neighbourData))
return error(self::NO_NEIGHBOUR);
$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);
}
/**
* Writes the whole order of one content in one transaction. Serves the drag and drop.
* The list must hold every child of the content exactly once, or the rows left out
* would keep a stale sort value.
* @param int $content_id
* @param array $contentchild_ids in the wanted order
* @return stdClass success(true) or error
*/
public function setSortOrder($content_id, $contentchild_ids)
{
$currentResult = $this->execReadOnlyQuery(
'SELECT contentchild_id FROM campus.tbl_contentchild WHERE content_id = ?',
[$content_id]
);
if (isError($currentResult))
return $currentResult;
$current = [];
foreach ((array) getData($currentResult) as $row)
$current[] = (int) $row->contentchild_id;
$wanted = array_map('intval', array_values($contentchild_ids));
$check = $wanted;
sort($current);
sort($check);
if ($current !== $check)
return error('Sort order does not match the children of the content');
$this->db->trans_start();
foreach ($wanted as $position => $contentchild_id)
{
$this->execQuery(
'UPDATE campus.tbl_contentchild SET sort = ? WHERE contentchild_id = ?',
[$position + 1, $contentchild_id]
);
}
$this->db->trans_complete();
if ($this->db->trans_status() === false)
return error('Sort order update failed');
return success(true);
}
/**
* Every ancestor of the given contents, with the distance to the content it belongs to.
*
* The tree shows a search result out of its place: a title like "Team" says nothing
* about which unit it belongs to. This delivers the way down to it.
*
* A content can hang under more than one parent, so a level can hold more than one
* ancestor. Every one of them comes back, with tiefe 1 for the direct parent. The
* caller decides how to draw that.
*
* tiefe also ends a cycle: a content whose parent chain leads back to itself would
* otherwise recurse forever.
*
* @param array $content_ids contents to walk up from
* @param int $maxTiefe how many levels to climb
* @param string $sprache language of the ancestor titles
* @return stdClass success with array of rows or error
*/
public function getAncestors($content_ids, $maxTiefe, $sprache)
{
if (empty($content_ids))
return success([]);
$platzhalter = implode(', ', array_fill(0, count($content_ids), '?'));
// The aggregate and the title sit in the final select. The recursive term names
// the working table once and carries no subquery, which is what Postgres allows.
$query = '
WITH RECURSIVE auf(start_id, content_id, tiefe) AS (
SELECT cc.child_content_id, cc.content_id, 1
FROM campus.tbl_contentchild cc
WHERE cc.child_content_id IN (' . $platzhalter . ')
UNION ALL
SELECT auf.start_id, cc.content_id, auf.tiefe + 1
FROM campus.tbl_contentchild cc
JOIN auf ON cc.child_content_id = auf.content_id
WHERE auf.tiefe < ?
)
SELECT auf.start_id, auf.content_id, MIN(auf.tiefe) AS tiefe,
(SELECT titel FROM campus.tbl_contentsprache cs
WHERE cs.content_id = auf.content_id AND cs.sprache = ?
ORDER BY cs.version LIMIT 1) AS titel,
-- The caller turns this into the entitled flag, so the path can offer a
-- link only for an ancestor the editor may open.
(SELECT c.oe_kurzbz FROM campus.tbl_content c
WHERE c.content_id = auf.content_id) AS oe_kurzbz
FROM auf
GROUP BY auf.start_id, auf.content_id
ORDER BY auf.start_id, MIN(auf.tiefe) DESC, auf.content_id
';
$params = array_map('intval', array_values($content_ids));
$params[] = (int) $maxTiefe;
$params[] = $sprache;
return $this->execReadOnlyQuery($query, $params);
}
/**
* Removes a content from the child table, as parent and as child.
* @param int $content_id
* @return stdClass
*/
public function deleteByContent($content_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentchild
WHERE content_id = ? OR child_content_id = ?',
[$content_id, $content_id]
);
}
}
@@ -10,8 +10,6 @@ class Contentgruppe_model extends DB_Model
parent::__construct();
$this->dbTable = 'campus.tbl_contentgruppe';
$this->pk = array('gruppe_kurzbz', 'content_id');
// Composite key without a sequence, so insert() must skip insert_id() and its LASTVAL().
$this->hasSequence = false;
}
/**
@@ -20,7 +18,7 @@ class Contentgruppe_model extends DB_Model
*
* @param int $content_id
*
* @return stdClass success(true) wenn locked sonst success(false)
* @return stdClass success(true) wenn eingeschraenkt sonst success(false)
*/
public function islocked($content_id)
{
@@ -32,7 +30,7 @@ class Contentgruppe_model extends DB_Model
}
/**
* Prüft ob ein User die Berechtigung fuer das Anzeigen des
* Prueft ob ein User die Berechtigung fuer das Anzeigen des
* Contents besitzt
*
* @param int $content_id ID des Contents
@@ -58,89 +56,5 @@ 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);
}
/**
* @param int $content_id
* @return stdClass
*/
public function deleteByContent($content_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentgruppe WHERE content_id = ?',
[$content_id]
);
}
}
@@ -11,76 +11,4 @@ class Contentlog_model extends DB_Model
$this->dbTable = 'campus.tbl_contentlog';
$this->pk = 'contentlog_id';
}
/**
* The open lock entry of a user (ende IS NULL). Returns null if none exists.
* Match the holder and take the newest, otherwise the age belongs to an abandoned entry.
* @param int $contentsprache_id
* @param string $uid holder of the lock
* @return stdClass success with row or null, or error
*/
public function getOpenLock($contentsprache_id, $uid)
{
$query = '
SELECT contentlog_id, uid, contentsprache_id, start
FROM campus.tbl_contentlog
WHERE contentsprache_id = ? AND uid = ? AND ende IS NULL
ORDER BY start DESC
LIMIT 1
';
$result = $this->execReadOnlyQuery($query, [$contentsprache_id, $uid]);
if (isError($result))
return $result;
$data = getData($result);
return success(empty($data) ? null : $data[0]);
}
/**
* Ends every open lock entry of a version. Without a uid it ends the entry of every
* holder, which is what a take-over and a forced release need.
* @param int $contentsprache_id
* @param string|null $uid
* @return stdClass
*/
public function closeOpenEntries($contentsprache_id, $uid = null)
{
$query = 'UPDATE campus.tbl_contentlog SET ende = now()
WHERE contentsprache_id = ? AND ende IS NULL';
$params = [$contentsprache_id];
if ($uid !== null)
{
$query .= ' AND uid = ?';
$params[] = $uid;
}
return $this->execQuery($query, $params);
}
/**
* @param int $contentsprache_id
* @return stdClass
*/
public function deleteByContentsprache($contentsprache_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentlog WHERE contentsprache_id = ?',
[$contentsprache_id]
);
}
/**
* @param int $content_id
* @return stdClass
*/
public function deleteByContent($content_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentlog WHERE contentsprache_id IN
(SELECT contentsprache_id FROM campus.tbl_contentsprache WHERE content_id = ?)',
[$content_id]
);
}
}
@@ -38,177 +38,5 @@ 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]);
}
/**
* Clears the lock of a version. Without a uid it clears whoever holds it, which is
* what a forced release needs.
* @param int $contentsprache_id
* @param string|null $uid
* @return stdClass
*/
public function releaseLock($contentsprache_id, $uid = null)
{
$query = 'UPDATE campus.tbl_contentsprache SET gesperrt_uid = NULL
WHERE contentsprache_id = ?';
$params = [$contentsprache_id];
if ($uid !== null)
{
$query .= ' AND gesperrt_uid = ?';
$params[] = $uid;
}
return $this->execQuery($query, $params);
}
/**
* @param int $content_id
* @return stdClass
*/
public function deleteByContent($content_id)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentsprache WHERE content_id = ?',
[$content_id]
);
}
/**
* @param int $content_id
* @param string $sprache
* @param int $version
* @return stdClass
*/
public function deleteVersion($content_id, $sprache, $version)
{
return $this->execQuery(
'DELETE FROM campus.tbl_contentsprache
WHERE content_id = ? AND sprache = ? AND version = ?',
[$content_id, $sprache, $version]
);
}
}
-237
View File
@@ -30,241 +30,4 @@ class Dms_model extends DB_Model
return $returnArray;
}
/**
* Reads a DMS category and the access facts a listing needs.
*
* Mirrors the rule of cms/dms.php and dms.class.php at category level:
* a category is locked when any category in its parent chain grants a group or
* carries a berechtigung_kurzbz. The caller decides, this method only reports.
*
* @param string $kategorie_kurzbz
* gesperrt and in_gruppe come back as 0 or 1, not as a boolean. A PostgreSQL boolean
* reaches PHP as the string 'f' when the driver conversion does not run, and 'f' is
* truthy. The int cast is correct in both cases.
*
* @param string $uid
* @return object one row, or an empty result when the category does not exist
*/
public function getKategorieZugriff($kategorie_kurzbz, $uid)
{
// tiefe guards against a cyclic parent chain.
$query = '
WITH RECURSIVE chain(kategorie_kurzbz, parent_kategorie_kurzbz, berechtigung_kurzbz, tiefe) AS (
SELECT kategorie_kurzbz, parent_kategorie_kurzbz, berechtigung_kurzbz, 1
FROM campus.tbl_dms_kategorie
WHERE kategorie_kurzbz = ?
UNION ALL
SELECT k.kategorie_kurzbz, k.parent_kategorie_kurzbz, k.berechtigung_kurzbz, c.tiefe + 1
FROM campus.tbl_dms_kategorie k
JOIN chain c ON k.kategorie_kurzbz = c.parent_kategorie_kurzbz
WHERE c.tiefe < 32
)
SELECT
kat.kategorie_kurzbz,
kat.bezeichnung,
kat.beschreibung,
kat.berechtigung_kurzbz,
(
EXISTS (SELECT 1 FROM chain WHERE chain.berechtigung_kurzbz IS NOT NULL)
OR EXISTS (
SELECT 1
FROM campus.tbl_dms_kategorie_gruppe g
JOIN chain ON chain.kategorie_kurzbz = g.kategorie_kurzbz
)
)::int AS gesperrt,
(EXISTS (
SELECT 1
FROM campus.tbl_dms_kategorie_gruppe g
JOIN chain ON chain.kategorie_kurzbz = g.kategorie_kurzbz
JOIN public.tbl_benutzergruppe b ON b.gruppe_kurzbz = g.gruppe_kurzbz
WHERE b.uid = ?
))::int AS in_gruppe
FROM campus.tbl_dms_kategorie kat
WHERE kat.kategorie_kurzbz = ?
';
return $this->execReadOnlyQuery($query, array($kategorie_kurzbz, $uid, $kategorie_kurzbz));
}
/**
* Lists the newest version of every document in one DMS category.
*
* filename stays out of the result. The reader gets the document through
* cms/dms.php?id=<dms_id>, never through the name on disk.
*
* @param string $kategorie_kurzbz
* @return object
*/
public function getKategorieDokumente($kategorie_kurzbz)
{
$query = '
SELECT
d.dms_id,
v.version,
v.name,
v.beschreibung,
v.mimetype,
COALESCE(v.updateamum, v.insertamum) AS geaendert
FROM campus.tbl_dms d
JOIN campus.tbl_dms_version v ON v.dms_id = d.dms_id
WHERE d.kategorie_kurzbz = ?
-- cis_suche marks a document as published to CIS readers. A category also
-- holds working files, and those must not appear on a content page.
AND v.cis_suche
AND v.version = (
SELECT MAX(version) FROM campus.tbl_dms_version WHERE dms_id = d.dms_id
)
-- A project document carries its own rule in dms.class.php and does not
-- belong in a CMS category listing.
AND NOT EXISTS (
SELECT 1 FROM fue.tbl_projekt_dokument pd WHERE pd.dms_id = d.dms_id
)
ORDER BY LOWER(v.name)
';
return $this->execReadOnlyQuery($query, array($kategorie_kurzbz));
}
/**
* Newest version of the named documents, in no particular order.
*
* Unlike getKategorieDokumente() this does NOT filter on cis_suche. An editor who names
* a dms_id has chosen that document on purpose, and the flag only marks a document for
* the CIS search. The category rule still applies, and the caller enforces it.
*
* @param array $ids dms_id values
* @return object
*/
public function getDokumenteByIds($ids)
{
if (empty($ids))
return success(array());
$platzhalter = implode(', ', array_fill(0, count($ids), '?'));
$query = '
SELECT
d.dms_id,
d.kategorie_kurzbz,
v.version,
v.name,
v.beschreibung,
v.mimetype,
COALESCE(v.updateamum, v.insertamum) AS geaendert
FROM campus.tbl_dms d
JOIN campus.tbl_dms_version v ON v.dms_id = d.dms_id
WHERE d.dms_id IN (' . $platzhalter . ')
AND v.version = (
SELECT MAX(version) FROM campus.tbl_dms_version WHERE dms_id = d.dms_id
)
AND NOT EXISTS (
SELECT 1 FROM fue.tbl_projekt_dokument pd WHERE pd.dms_id = d.dms_id
)
';
return $this->execReadOnlyQuery($query, array_values($ids));
}
/**
* Every document of one category, for the editor picker.
*
* Unlike getKategorieDokumente() this does NOT filter on cis_suche, because
* dms-dokumente renders named documents regardless of that flag. The flag travels with
* the row so the picker can mark a document that no reader would find by search.
*
* @param string $kategorie_kurzbz
* @return object
*/
public function getKategorieDokumenteAlle($kategorie_kurzbz)
{
$query = '
SELECT
d.dms_id,
v.name,
v.cis_suche::int AS cis_suche
FROM campus.tbl_dms d
JOIN LATERAL (
SELECT v.name, v.cis_suche
FROM campus.tbl_dms_version v
WHERE v.dms_id = d.dms_id
ORDER BY v.version DESC
LIMIT 1
) v ON TRUE
WHERE d.kategorie_kurzbz = ?
AND NOT EXISTS (
SELECT 1 FROM fue.tbl_projekt_dokument pd WHERE pd.dms_id = d.dms_id
)
ORDER BY LOWER(v.name)
';
return $this->execReadOnlyQuery($query, array($kategorie_kurzbz));
}
/**
* Lists every DMS category with the number of documents it holds.
* Used by the insert dialog of the CMS content component "dms-liste".
*
* @return object
*/
public function getKategorien()
{
$query = '
SELECT
k.kategorie_kurzbz,
k.bezeichnung,
k.oe_kurzbz,
COALESCE(a.anzahl, 0) AS anzahl
FROM campus.tbl_dms_kategorie k
LEFT JOIN (
SELECT d.kategorie_kurzbz, COUNT(*) AS anzahl
FROM campus.tbl_dms d
JOIN (
SELECT DISTINCT ON (dms_id) dms_id, cis_suche
FROM campus.tbl_dms_version
ORDER BY dms_id, version DESC
) v ON v.dms_id = d.dms_id
WHERE v.cis_suche
GROUP BY d.kategorie_kurzbz
) a ON a.kategorie_kurzbz = k.kategorie_kurzbz
ORDER BY LOWER(COALESCE(k.bezeichnung, k.kategorie_kurzbz))
';
return $this->execReadOnlyQuery($query);
}
/**
* The category itself plus its sub categories, down to a depth.
*
* tiefe 0 returns only the category named. The depth bound also ends a cycle: a
* category whose parent chain leads back to itself would otherwise recurse forever.
*
* The caller must still check the entitlement of every category it gets back. A sub
* category carries its own group rule and may be closed while the parent is open.
*
* @param string $kategorie_kurzbz root of the walk
* @param int $tiefe how many levels below the root to include
* @return object success with array of rows or error
*/
public function getMitUnterkategorien($kategorie_kurzbz, $tiefe)
{
$query = '
WITH RECURSIVE unter(kategorie_kurzbz, tiefe) AS (
SELECT kategorie_kurzbz, 0
FROM campus.tbl_dms_kategorie
WHERE kategorie_kurzbz = ?
UNION ALL
SELECT k.kategorie_kurzbz, unter.tiefe + 1
FROM campus.tbl_dms_kategorie k
JOIN unter ON k.parent_kategorie_kurzbz = unter.kategorie_kurzbz
WHERE unter.tiefe < ?
)
SELECT kategorie_kurzbz, MIN(tiefe) AS tiefe
FROM unter
GROUP BY kategorie_kurzbz
ORDER BY MIN(tiefe), kategorie_kurzbz
';
return $this->execReadOnlyQuery($query, [$kategorie_kurzbz, (int) $tiefe]);
}
}
@@ -11,30 +11,4 @@ class Template_model extends DB_Model
$this->dbTable = 'campus.tbl_template';
$this->pk = 'template_kurzbz';
}
/**
* True if the template carries a CIS4 stylesheet of its own, which is not true for every template.
*
* @param string $template_kurzbz
* @return stdClass success(bool) or error
*/
public function hasCis4Stylesheet($template_kurzbz)
{
$query = '
SELECT (xslt_xhtml_c4 IS NOT NULL
AND xslt_xhtml_c4::text IS DISTINCT FROM xslt_xhtml::text) AS eigen
FROM campus.tbl_template
WHERE template_kurzbz = ?
';
$result = $this->execReadOnlyQuery($query, [$template_kurzbz]);
if (isError($result))
return $result;
$data = getData($result);
if (empty($data))
return success(false);
return success($data[0]->eigen === 't' || $data[0]->eigen === true);
}
}
+1 -56
View File
@@ -248,60 +248,5 @@ class Akte_model extends DB_Model
return success($dokumente->retval);
}
/**
* Liefert die Archivdokumente einer Person/mehrerer Personen
*
* @param integer/array $person_id
* @param boolean|null $signiert Wenn true werden nur Dokumente geliefert die digital signiert wurden.
* @param boolean|null $stud_selfservice Wenn true werden nur Dokumente geliefert die Studierende selbst herunterladen duerfen.
*
* @return stdClass
*/
public function getArchiv($person_id, $signiert = null, $stud_selfservice = null)
{
$this->addSelect('akte_id');
$this->addSelect('person_id');
$this->addSelect('dokument_kurzbz');
$this->addSelect('mimetype');
$this->addSelect('erstelltam');
$this->addSelect('gedruckt');
$this->addSelect('titel_intern');
$this->addSelect('anmerkung_intern');
$this->addSelect('titel');
$this->addSelect('bezeichnung');
$this->addSelect('updateamum');
$this->addSelect('insertamum');
$this->addSelect('updatevon');
$this->addSelect('insertvon');
$this->addSelect('uid');
$this->addSelect('dms_id');
$this->addSelect('anmerkung');
$this->addSelect('nachgereicht');
$this->addSelect('CASE WHEN inhalt is not null THEN true ELSE false END as inhalt_vorhanden', false);
$this->addSelect('nachgereicht_am');
$this->addSelect('ausstellungsnation');
$this->addSelect('formal_geprueft_amum');
$this->addSelect('archiv');
$this->addSelect('signiert');
$this->addSelect('stud_selfservice');
$this->addSelect('akzeptiertamum');
if ($signiert !== null)
$this->db->where('signiert', (boolean)$signiert);
if ($stud_selfservice !== null)
$this->db->where('stud_selfservice', (boolean)$stud_selfservice);
if (is_array($person_id))
$this->db->where_in('person_id', $person_id);
else
$this->db->where('person_id', $person_id);
$this->addOrder('erstelltam', 'DESC');
$this->addOrder('akte_id', 'DESC');
return $this->loadWhere([
'archiv' => true
]);
}
}
@@ -259,37 +259,4 @@ class Organisationseinheit_model extends DB_Model
return $this->execReadOnlyQuery($qry, array($oe_kurzbz));
}
/**
* The unit itself plus its sub units, down to a depth.
*
* tiefe 0 returns only the unit named, which is what a caller without a depth wants.
* The depth bound also ends a cycle: a unit whose parent chain leads back to itself
* would otherwise recurse forever.
*
* @param string $oe_kurzbz root of the walk
* @param int $tiefe how many levels below the root to include
* @return object success with array of rows or error
*/
public function getMitUntereinheiten($oe_kurzbz, $tiefe)
{
$query = '
WITH RECURSIVE unter(oe_kurzbz, tiefe) AS (
SELECT oe_kurzbz, 0
FROM public.tbl_organisationseinheit
WHERE oe_kurzbz = ?
UNION ALL
SELECT o.oe_kurzbz, unter.tiefe + 1
FROM public.tbl_organisationseinheit o
JOIN unter ON o.oe_parent_kurzbz = unter.oe_kurzbz
WHERE unter.tiefe < ?
)
SELECT oe_kurzbz, MIN(tiefe) AS tiefe
FROM unter
GROUP BY oe_kurzbz
ORDER BY MIN(tiefe), oe_kurzbz
';
return $this->execReadOnlyQuery($query, [$oe_kurzbz, (int) $tiefe]);
}
}
@@ -292,13 +292,29 @@ class Studiensemester_model extends DB_Model
COALESCE(
(SELECT studiensemester_kurzbz FROM semester_interval_startdate),
(SELECT studiensemester_kurzbz FROM nearest_semester_interval_startdate)
) AS studiensemester_kurzbz
) AS studiensemester_kurzbz,
COALESCE(
(SELECT start FROM semester_interval_startdate),
(SELECT start FROM nearest_semester_interval_startdate)
) as start,
COALESCE(
(SELECT ende FROM semester_interval_startdate),
(SELECT ende FROM nearest_semester_interval_startdate)
) as ende
UNION
SELECT
COALESCE(
(SELECT studiensemester_kurzbz FROM semester_interval_enddate),
(SELECT studiensemester_kurzbz FROM nearest_semester_interval_enddate)
) AS studiensemester_kurzbz
) AS studiensemester_kurzbz,
COALESCE(
(SELECT start FROM semester_interval_enddate),
(SELECT start FROM nearest_semester_interval_enddate)
) as start,
COALESCE(
(SELECT ende FROM semester_interval_enddate),
(SELECT ende FROM nearest_semester_interval_enddate)
) as ende
EOSQL;
@@ -243,58 +243,6 @@ class Person_model extends DB_Model
* @param $uid
* @return array
*/
/**
* Contact data of the named users, for the CMS content components.
*
* The joins follow Benutzerfunktion_model::getBenutzerFunktionenDetailed() and the view
* Cis/Cms/News/Xml/Address/Detailed.php, so a person reads the same everywhere.
*
* foto_sperre is honoured. A person who blocked their photo never gets one on a public
* content page, no matter what the page asks for.
*
* @param array $uids
* @param boolean $mitFoto photos are large, so they are read only on request
* @return object
*/
public function getCmsKontakt($uids, $mitFoto = false)
{
if (empty($uids))
return success(array());
$platzhalter = implode(', ', array_fill(0, count($uids), '?'));
$fotoSpalte = $mitFoto
? 'CASE WHEN p.foto_sperre THEN NULL ELSE p.foto END'
: 'NULL';
$query = '
SELECT
b.uid,
b.alias,
p.vorname,
p.nachname,
p.titelpre,
p.titelpost,
m.telefonklappe,
k.kontakt,
o.planbezeichnung,
' . $fotoSpalte . ' AS foto
FROM public.tbl_benutzer b
JOIN public.tbl_person p ON p.person_id = b.person_id
LEFT JOIN public.tbl_mitarbeiter m ON m.mitarbeiter_uid = b.uid
LEFT JOIN public.tbl_kontakt k
ON k.standort_id = m.standort_id AND k.kontakttyp = ?
LEFT JOIN public.tbl_ort o ON o.ort_kurzbz = m.ort_kurzbz
WHERE b.uid IN (' . $platzhalter . ')
AND b.aktiv
';
$parameter = array('telefon');
foreach ($uids as $uid)
$parameter[] = $uid;
return $this->execReadOnlyQuery($query, $parameter);
}
public function getByUid($uid)
{
$this->addSelect('vorname, nachname, gebdatum, person_id, bpk, matr_nr, foto');
@@ -98,31 +98,6 @@ class Mitarbeiter_model extends DB_Model
return $this->execQuery($qry, $params);
}
/**
* Active staff, names only, for a picker.
*
* getPersonal() returns tbl_person.* including svnr, bpk and
* gebdatum, which is not needed in a CMS endpoint.
*
* @return object
*/
public function getAktivesPersonalNamen()
{
$query = '
SELECT
b.uid,
p.vorname,
p.nachname
FROM public.tbl_benutzer b
JOIN public.tbl_mitarbeiter m ON m.mitarbeiter_uid = b.uid
JOIN public.tbl_person p ON p.person_id = b.person_id
WHERE b.aktiv
ORDER BY LOWER(p.nachname), LOWER(p.vorname)
';
return $this->execReadOnlyQuery($query);
}
/**
* gibt Personen mit Übersicht von Vertragsdaten aus
*
@@ -12,72 +12,4 @@ class Webservicelog_model extends DB_Model
$this->dbTable = 'system.tbl_webservicelog';
$this->pk = 'webservicelog_id';
}
/**
* The most viewed contents since a date, with the title of each one.
* request_id is a varchar, so the join casts the content_id to text. A cast of
* request_id to a number would fail on a row that holds no number.
* The join also drops a log row whose content is deleted.
* @param string|null $since timestamp in the form Y-m-d H:i:s, null for the whole log
* @param string $sprache language of the title
* @param int $limit length of the ranking
* @return stdClass success with array of rows or error
*/
public function getContentClickCounts($since, $sprache, $limit)
{
$params = [$sprache, 'content'];
$sinceCondition = '';
if ($since !== null)
{
$sinceCondition = ' AND w.execute_time >= ?';
$params[] = $since;
}
$params[] = $limit;
$query = '
SELECT c.content_id, COUNT(*) AS hits,
(SELECT titel FROM campus.tbl_contentsprache
WHERE content_id = c.content_id AND sprache = ?
ORDER BY version LIMIT 1) AS titel
FROM system.tbl_webservicelog w
JOIN campus.tbl_content c ON c.content_id::text = w.request_id
WHERE w.webservicetyp_kurzbz = ?' . $sinceCondition . '
GROUP BY c.content_id
ORDER BY COUNT(*) DESC, c.content_id
LIMIT ?
';
return $this->execReadOnlyQuery($query, $params);
}
/**
* Views of one content since a date. A content outside the ranking still needs its
* own number.
* @param int $content_id
* @param string|null $since timestamp in the form Y-m-d H:i:s, null for the whole log
* @return stdClass success with int or error
*/
public function getContentClickCount($content_id, $since)
{
$query = '
SELECT COUNT(*) AS hits
FROM system.tbl_webservicelog
WHERE webservicetyp_kurzbz = ? AND request_id = ?
';
$params = ['content', (string) $content_id];
if ($since !== null)
{
$query .= ' AND execute_time >= ?';
$params[] = $since;
}
$result = $this->execReadOnlyQuery($query, $params);
if (isError($result))
return $result;
return success((int) getData($result)[0]->hits);
}
}
@@ -23,7 +23,6 @@ $includesArray = array(
'public/css/components/FormUnderline.css',
'public/css/components/abgabetool/abgabe.css',
'public/css/Cis4/Cms.css',
'public/css/Cis4/Contentcomponents.css',
'public/css/Cis4/Studium.css'
),
'customJSs' => array(
-35
View File
@@ -1,35 +0,0 @@
<?php
$includesArray = array(
'title' => '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/components/horizontalsplit.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);
?>
<div id="main">
<router-view
cms-root="<?= site_url('cms'); ?>"
auth-uid="<?= getAuthUID(); ?>"
default-language="<?= defined('DEFAULT_LANGUAGE') ? DEFAULT_LANGUAGE : 'German'; ?>"
<?= !empty($clickstats) ? 'clickstats' : ''; ?>
></router-view>
</div>
<?php $this->load->view('templates/FHC-Footer', $includesArray); ?>
-28
View File
@@ -83,40 +83,12 @@ if (! $rechte->isberechtigt('basis/dms', null, 's', null))
return confirm('Möchten Sie das File wirklich löschen?');
}
/*
* CIS4 bridge, ticket 78489.
*
* The TinyMCE 5 editor of the new CMS admin cannot receive a selection through the
* TinyMCE 3 API below, so this window posts the URL back to its opener instead.
*
* message: { typ: 'fhc-dms-selection', url: 'dms.php?id=<dms_id>' }
* target: window.opener, origin checked on both sides
*
* The path is chosen by window.name, which the opener sets and which survives the
* internal navigation of this browser.
*
* 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. Do not test for
// tinyMCEPopup, this page loads tiny_mce_popup.js itself and always defines it.
if (window.name === 'DMS_CIS4' && 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
+63 -25
View File
@@ -21,6 +21,7 @@
* Manuela Thamer <[email protected]>
*/
require_once(dirname(__FILE__).'/basis_db.class.php');
require_once(dirname(__FILE__).'/mitarbeiter.class.php');
class entwicklungsteam extends basis_db
{
@@ -43,7 +44,6 @@ class entwicklungsteam extends basis_db
public $ext_id;
public $besqual;
public $studiengang_kz_old;
/**
* Konstruktor
@@ -74,7 +74,7 @@ class entwicklungsteam extends basis_db
$qry = "SELECT * FROM bis.tbl_entwicklungsteam JOIN bis.tbl_besqual USING(besqualcode)
WHERE entwicklungsteam_id=".$this->db_add_param($entwicklungsteam_id);
$qry.=";";
$qry.=";";
if($this->db_query($qry))
{
@@ -147,6 +147,11 @@ class entwicklungsteam extends basis_db
$this->errormsg = 'Es muss ein Mitarbeiter angegeben werden';
return false;
}
if(!$this->checkIfExistingUid($this->mitarbeiter_uid))
{
$this->errormsg = 'Bitte einen Eintrag aus dem Dropdown Personensuche auswählen!';
return false;
}
if($this->studiengang_kz=='')
{
$this->errormsg = 'Es muss ein Studiengang angegeben werden';
@@ -162,6 +167,26 @@ class entwicklungsteam extends basis_db
$this->errormsg = 'Endedatum darf nicht vor Anfangsdatum liegen';
return false;
}
if($this->beginn != '')
{
$beginnParts = explode('-', $this->beginn);
if (!checkdate((int)$beginnParts[1], (int)$beginnParts[2], (int)$beginnParts[0])) {
$this->errormsg = 'Anfangsdatum ist ungültig';
return false;
}
}
if($this->ende != '')
{
$endeParts = explode('-', $this->ende);
if (!checkdate((int)$endeParts[1], (int)$endeParts[2], (int)$endeParts[0]))
{
$this->errormsg = 'Endedatum ist ungültig';
return false;
}
}
return true;
}
@@ -183,30 +208,28 @@ class entwicklungsteam extends basis_db
//Neuen Datensatz anlegen
$qry = "INSERT INTO bis.tbl_entwicklungsteam (mitarbeiter_uid, studiengang_kz, besqualcode, beginn, ende,
updateamum, updatevon, insertamum, insertvon) VALUES (".
$this->db_add_param($this->mitarbeiter_uid).', '.
$this->db_add_param($this->studiengang_kz, FHC_INTEGER).', '.
$this->db_add_param($this->besqualcode, FHC_INTEGER).', '.
$this->db_add_param($this->beginn).', '.
$this->db_add_param($this->ende).', '.
$this->db_add_param($this->updateamum).', '.
$this->db_add_param($this->updatevon).', '.
$this->db_add_param($this->insertamum).', '.
$this->db_add_param($this->insertvon).');';
$this->db_add_param($this->mitarbeiter_uid).', '.
$this->db_add_param($this->studiengang_kz, FHC_INTEGER).', '.
$this->db_add_param($this->besqualcode, FHC_INTEGER).', '.
$this->db_add_param($this->beginn).', '.
$this->db_add_param($this->ende).', '.
$this->db_add_param($this->updateamum).', '.
$this->db_add_param($this->updatevon).', '.
$this->db_add_param($this->insertamum).', '.
$this->db_add_param($this->insertvon).');';
}
else
{
if($this->studiengang_kz_old=='')
$this->studiengang_kz_old = $this->studiengang_kz;
//Bestehenden Datensatz aktualisieren
$qry= "UPDATE bis.tbl_entwicklungsteam SET".
" besqualcode=".$this->db_add_param($this->besqualcode, FHC_INTEGER).",".
" beginn=".$this->db_add_param($this->beginn).",".
" studiengang_kz=".$this->db_add_param($this->studiengang_kz, FHC_INTEGER).",".
" ende=".$this->db_add_param($this->ende).",".
" updateamum=".$this->db_add_param($this->updateamum).",".
" updatevon=".$this->db_add_param($this->updatevon).
" WHERE entwicklungsteam_id=".$this->db_add_param($this->entwicklungsteam_id).";";
" besqualcode=".$this->db_add_param($this->besqualcode, FHC_INTEGER).",".
" beginn=".$this->db_add_param($this->beginn).",".
" studiengang_kz=".$this->db_add_param($this->studiengang_kz, FHC_INTEGER).",".
" ende=".$this->db_add_param($this->ende).",".
" mitarbeiter_uid=".$this->db_add_param($this->mitarbeiter_uid).",".
" updateamum=".$this->db_add_param($this->updateamum).",".
" updatevon=".$this->db_add_param($this->updatevon).
" WHERE entwicklungsteam_id=".$this->db_add_param($this->entwicklungsteam_id).";";
}
if($this->db_query($qry))
@@ -234,7 +257,7 @@ class entwicklungsteam extends basis_db
if($studiengang_kz!=null)
$qry.=" AND studiengang_kz=".$this->db_add_param($studiengang_kz);
$qry.=";";
$qry.=";";
if($this->db_query($qry))
{
@@ -316,7 +339,7 @@ class entwicklungsteam extends basis_db
if ($sort != null)
{
$qry .= " ORDER BY ".$sort;
$qry .= " ORDER BY ".$sort;
}
$qry .= ";";
@@ -364,7 +387,7 @@ class entwicklungsteam extends basis_db
$bismeldung_jahr = $datetime->format('Y');
//laden des Datensatzes
$qry = "SELECT tbl_entwicklungsteam.*, tbl_besqual.*, tbl_studiengang.studiengang_kz, tbl_studiengang.melderelevant
$qry = "SELECT tbl_entwicklungsteam.*, tbl_besqual.*, tbl_studiengang.studiengang_kz, tbl_studiengang.melderelevant
FROM bis.tbl_entwicklungsteam
JOIN bis.tbl_besqual USING(besqualcode)
JOIN public.tbl_studiengang USING(studiengang_kz)
@@ -375,7 +398,7 @@ class entwicklungsteam extends basis_db
if($studiengang_kz!=null)
$qry.=" AND studiengang_kz=".$this->db_add_param($studiengang_kz);
$qry.=";";
$qry.=";";
if($this->db_query($qry))
{
@@ -407,5 +430,20 @@ class entwicklungsteam extends basis_db
return false;
}
}
/*
* Überprüft, ob die übergebene Uid in der Tabelle tbl_mitarbeiter vorhanden ist
* @param String $uid UID des Mitarbeiters
* @return true wenn vorhanden, false wenn nicht
*/
private function checkIfExistingUid($uid)
{
$mitarbeiter = new mitarbeiter();
if(!$mitarbeiter->checkIfExistingUid($uid))
return false;
return true;
}
}
?>
+18
View File
@@ -1995,5 +1995,23 @@ class mitarbeiter extends benutzer
return true;
}
}
/**
* Prueft ob eine existierende ma_uid übergeben wurde
* @param String $mitarbeiter_uid mitarbeiter_uid
* @return bool True wenn mitarbeiter_uid vorhanden sonst false.
*/
public function checkIfExistingUid($mitarbeiter_uid)
{
$qry = "Select 1 from public.tbl_mitarbeiter where mitarbeiter_uid =".$this->db_add_param($mitarbeiter_uid);
if ($result = $this->db_query($qry))
{
if ($this->db_num_rows($result) > 0)
return true;
else
return false;
}
}
}
?>
+17 -1
View File
@@ -853,4 +853,20 @@ html {
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
}
.cis-main-menu {
max-width: 100vw;
}
.cis-main-menu ul {
list-style: none;
margin: 0;
padding: 0;
}
.cis-main-menu ul li {
margin: 0;
padding: 0;
width: 100%;
}
-196
View File
@@ -1,196 +0,0 @@
/* Contentcomponents in CIS4 content.
*
* A contentcomponent renders inside its marker div, below #fhc-cms-content. The
* components themselves carry Bootstrap utility classes only, so without this file they
* read as unstyled Bootstrap.
*
* The class names are the contract with the components. A root class follows the pattern
* fhc-contentcomponent-<name> and is set in the template of the component.
*
* Colours come from the theme. public/css/theme/default.css declares the semantic
* variables under :root.light and :root.dark, never on bare :root, so every var() below
* carries a fallback. The fallback repeats the light value.
*
* Two themes are covered: light and dark. purple takes the light values.
*/
:root {
--fhc-cc-muted: #6c757d;
--fhc-cc-accent: var(--fhc-primary, #006095);
--fhc-cc-surface: var(--fhc-secondary, #f6f6f6);
--fhc-cc-hover: var(--fhc-tertiary, #e9ecef);
--fhc-cc-border: var(--fhc-border, #dee2e6);
--fhc-cc-radius: 0.3rem;
}
/* Two values need a step of their own. #006095 and #6c757d both fall under 3:1 on the
dark background, which is too little even for secondary text and for an icon. */
:root.dark {
--fhc-cc-muted: var(--fhc-white-90, #aeb3b8);
--fhc-cc-accent: var(--fhc-blue-40, #61aed8);
}
/* --------------------------------------------------------------------------------- */
/* The marker */
/* --------------------------------------------------------------------------------- */
/* The marker stays in the DOM and wraps the component. scan.js empties it, Teleport
renders the component root into it. It is therefore the place for the outer spacing. */
.fhc-contentcomponent {
display: block;
margin: 1.25rem 0;
}
.fhc-contentcomponent:first-child {
margin-top: 0;
}
.fhc-contentcomponent:last-child {
margin-bottom: 0;
}
.fhc-contentcomponent .alert {
margin-bottom: 0;
}
/* --------------------------------------------------------------------------------- */
/* Person block */
/* --------------------------------------------------------------------------------- */
/* PersonBlock.js. It carries person-block and every row of oe-personen. */
.fhc-contentcomponent-person {
/* flow-root contains the floated photo. It spares the clearfix element, which the
component does not render. */
display: flow-root;
padding: 0.75rem 1rem;
border: 1px solid var(--fhc-cc-border);
border-radius: var(--fhc-cc-radius);
background-color: var(--fhc-cc-surface);
/* The block carries mb-3. The gap of the two lists below owns the spacing instead,
so the last block does not push the content under it. Bootstrap writes the utility
with !important, so this needs it as well. */
margin-bottom: 0 !important;
}
/* The photo carries its width and its object-fit as an inline style. No rule here can
beat that, so only the placement is set. */
.fhc-contentcomponent-person > img {
float: left;
margin-right: 1rem;
border-radius: var(--fhc-cc-radius);
}
/* The name. It is the first div, with or without a photo above it. */
.fhc-contentcomponent-person > div:first-of-type {
font-size: 1.05em;
font-weight: 600;
line-height: 1.4;
}
/* Function, phone, room and mail. The mail and phone links keep the link colour. */
.fhc-contentcomponent-person > div:not(:first-of-type) {
font-size: 0.95em;
color: var(--fhc-cc-muted);
}
/* --------------------------------------------------------------------------------- */
/* oe-personen, person-block */
/* --------------------------------------------------------------------------------- */
.fhc-contentcomponent-oepersonen,
.fhc-contentcomponent-personkontakt {
display: grid;
gap: 0.75rem;
}
/* --------------------------------------------------------------------------------- */
/* dms-liste, dms-dokumente */
/* --------------------------------------------------------------------------------- */
/* Both render DokumentListe.js: ul.list-unstyled with one li per document. */
.fhc-contentcomponent-dmsliste ul,
.fhc-contentcomponent-dmsdokumente ul {
border: 1px solid var(--fhc-cc-border);
border-radius: var(--fhc-cc-radius);
/* Keeps the first and the last row inside the rounded corners. */
overflow: hidden;
}
.fhc-contentcomponent-dmsliste li,
.fhc-contentcomponent-dmsdokumente li {
padding: 0.5rem 0.75rem;
border-top: 1px solid var(--fhc-cc-border);
/* The row carries mb-2. The line above separates the rows instead. */
margin-bottom: 0 !important;
}
.fhc-contentcomponent-dmsliste li:first-child,
.fhc-contentcomponent-dmsdokumente li:first-child {
border-top: 0;
}
.fhc-contentcomponent-dmsliste li:hover,
.fhc-contentcomponent-dmsdokumente li:hover {
background-color: var(--fhc-cc-surface);
}
/* The file type icon sits inside the link and would take the link colour. A fixed width
lines the names up over every row, whatever the icon is. */
.fhc-contentcomponent-dmsliste li i,
.fhc-contentcomponent-dmsdokumente li i {
width: 1.25rem;
margin-right: 0.25rem;
text-align: center;
color: var(--fhc-cc-accent);
}
.fhc-contentcomponent-dmsliste li a,
.fhc-contentcomponent-dmsdokumente li a {
text-decoration: none;
}
.fhc-contentcomponent-dmsliste li a:hover,
.fhc-contentcomponent-dmsliste li a:focus,
.fhc-contentcomponent-dmsdokumente li a:hover,
.fhc-contentcomponent-dmsdokumente li a:focus {
text-decoration: underline;
}
/* The description is the only block element in the row. It starts below the icon, so it
takes the width of the icon as an indent and lines up with the document name. */
.fhc-contentcomponent-dmsliste li > div,
.fhc-contentcomponent-dmsdokumente li > div {
padding-left: 1.5rem;
}
/* --------------------------------------------------------------------------------- */
/* contentchild-menu */
/* --------------------------------------------------------------------------------- */
/* The successor of the legacy menubox. The accent rule replaces its grey background as
the mark that says: this is a menu, not running text. */
.fhc-contentcomponent-contentchildmenu ul {
padding: 0.5rem;
border-left: 3px solid var(--fhc-cc-accent);
border-radius: 0 var(--fhc-cc-radius) var(--fhc-cc-radius) 0;
background-color: var(--fhc-cc-surface);
}
.fhc-contentcomponent-contentchildmenu li {
/* The entry carries mb-1. The link padding owns the spacing instead. */
margin-bottom: 0 !important;
}
/* A block link makes the whole row the target, not the length of the title. */
.fhc-contentcomponent-contentchildmenu a {
display: block;
padding: 0.35rem 0.5rem;
border-radius: var(--fhc-cc-radius);
text-decoration: none;
}
.fhc-contentcomponent-contentchildmenu a:hover,
.fhc-contentcomponent-contentchildmenu a:focus {
background-color: var(--fhc-cc-hover);
text-decoration: underline;
}
-97
View File
@@ -1,97 +0,0 @@
/* Full-height shell. Without a height chain down to .cms-admin the page itself scrolls
and the tabs leave the screen while paging through the tree. */
html,
body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
overflow: hidden;
}
#main {
flex: 1 1 auto;
min-height: 0;
}
.cms-admin {
min-height: 0;
}
/* The split component only caps its height with max-height, so give it a definite one. */
.cms-admin .horizontalsplit-container {
height: 100%;
}
.cms-admin .horizontalsplitted {
min-width: 0;
overscroll-behavior: contain;
}
.cms-admin-main {
min-width: 0;
overflow: hidden;
}
.cms-admin-tab-content {
min-height: 0;
}
/* Anchor for the overlay that blocks the view during a content switch. */
.cms-admin {
position: relative;
}
.cms-admin-overlay {
position: absolute;
inset: 0;
z-index: 1050;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.6);
cursor: wait;
}
/* content locker to groups get a gold padlock - resembles legacy cms */
.cms-tree-lock {
color: #f0c000;
text-shadow: 0 0 1px #8a6000;
}
/* A frame swallows the mouse events that reach it, so dragging the split handle over the
preview ends the drag. Both frames go through for the length of the drag, so the events
reach the page underneath. Only a drag sets this class, so the preview stays clickable
the rest of the time. */
.cms-preview-dragging iframe {
pointer-events: none;
}
/* The path down to a search hit, and the subtree under it. Both are context, so they step
back and let the hit itself carry the normal weight. */
.cms-tree-ancestor {
font-size: 0.8em;
color: #6c757d;
line-height: 1.4;
}
/* The path is context and keeps the grey, but it leads somewhere, so it underlines on
hover like any other link. */
.cms-tree-ancestor a {
color: inherit;
text-decoration: none;
}
.cms-tree-ancestor a:hover,
.cms-tree-ancestor a:focus {
color: var(--bs-link-color, #0d6efd);
text-decoration: underline;
}
.cms-tree-context > div > a,
.cms-tree-context > div > span {
font-size: 0.85em;
color: #6c757d;
}
+2 -51
View File
@@ -16,8 +16,7 @@
*/
export default {
// preview flag to suppress click log entry. The CMS admin preview passes it.
content(content_id, version=null, sprache=null, sichtbar=null, preview=false) {
content(content_id, version=null, sprache=null, sichtbar=null) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/content',
@@ -25,8 +24,7 @@ export default {
content_id,
...(version ? { version } : {}),
...(sprache ? { sprache } : {}),
...(sichtbar ? { sichtbar } : {}),
...(preview ? { preview: 1 } : {})
...(sichtbar ? { sichtbar } : {})
}
};
},
@@ -56,53 +54,6 @@ export default {
url: '/api/frontend/v1/Cms/getNewsRowCount'
};
},
// Used by the CMS content component "oe-personen".
// tiefe 0 asks about the one unit, higher takes that many levels of sub units with it.
getOePersonen(oe_kurzbz, foto, tiefe) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getOePersonen',
params: {
oe_kurzbz,
...(foto ? { foto: 1 } : {}),
...(tiefe ? { tiefe } : {})
}
};
},
// Used by the CMS content component "person-block".
getPerson(uid, foto) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getPerson',
params: { uid, ...(foto ? { foto: 1 } : {}) }
};
},
// Used by the CMS content component "dms-dokumente".
getDmsDokumente(dms_ids) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getDmsDokumente',
params: { dms_ids }
};
},
// Used by the CMS content component "dms-liste".
// tiefe 0 asks about the one category, higher takes that many levels of sub categories
// with it. The server checks the entitlement of every category it reaches.
getDmsKategorie(kategorie_kurzbz, tiefe) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getDmsKategorie',
params: { kategorie_kurzbz, ...(tiefe ? { tiefe } : {}) }
};
},
// Used by the CMS content component "contentchild-menu".
getContentChilds(content_id, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/Cms/getContentChilds',
params: { content_id, ...(sprache ? { sprache } : {}) }
};
},
getNewsExtra() {
// TODO(chris): seems to be called from nowhere?
return {
-296
View File
@@ -1,296 +0,0 @@
export default {
// --- CmsAdmin ---
getTree(menu, filter) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getTree',
params: { menu, ...(filter ? { filter } : {}) }
};
},
getSubtree(content_id) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getSubtree',
params: { content_id }
};
},
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 }
};
},
// Feeds the tree filter: child count, insertamum, updateamum and the own-edit flag.
getTreeMeta() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getTreeMeta'
};
},
// Where each content sits. The tree asks for the whole visible list at once, so the
// hierarchy toggle costs one request and not one per node.
getAncestors(content_ids) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getAncestors',
params: { content_ids: content_ids.join(',') }
};
},
// Views of one content. One counting query, so the tab loads it right away.
// The server gates both click endpoints on the config flag and on the admin right,
// so a caller without both gets an error instead of numbers.
getClickCount(content_id, months) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getClickCount',
params: { content_id, months }
};
},
// The ranking over the whole log. Takes seconds, so the tab asks for it on request.
getClickRanking(months, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getClickRanking',
params: { months, sprache }
};
},
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'
};
},
// Fills the DMS category field of the contentcomponent insert dialog.
getDmsKategorien() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getDmsKategorien'
};
},
// Fills the person field of the contentcomponent insert dialog.
getMitarbeiter() {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getMitarbeiter'
};
},
// Fills the document field of the contentcomponent insert dialog.
getDmsKategorieDokumente(kategorie_kurzbz) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdmin/getDmsKategorieDokumente',
params: { kategorie_kurzbz }
};
},
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 }
};
},
// The hierarchy tab lists them above the children. A content can have several parents.
getParents(content_id, sprache) {
return {
method: 'get',
url: '/api/frontend/v1/CmsAdminStruktur/getParents',
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 }
};
},
// Writes the whole order after a drag and drop. The ids carry the wanted order.
putChildOrder(content_id, contentchild_ids) {
return {
method: 'post',
url: '/api/frontend/v1/CmsAdminStruktur/putChildOrder',
params: { content_id, contentchild_ids }
};
}
};
+11 -1
View File
@@ -44,5 +44,15 @@ export default {
studiensemester_kurzbz
}
};
}
},
getContainingOrNearestByDateRange(from, to) {
return {
method: 'get',
url: 'api/frontend/v1/organisation/studiensemester/getContainingOrNearestByDateRange',
params: {
from,
to,
}
};
},
};
+1 -6
View File
@@ -150,8 +150,7 @@ const router = VueRouter.createRouter({
}
},
{
// optional version for cms admin preview
path: `/CisVue/Cms/Content/:content_id/:version?`,
path: `/CisVue/Cms/Content/:content_id`,
name: 'Content',
component: CmsContent,
props: true
@@ -351,9 +350,5 @@ app.directive('contrast', contrast);
app.mount('#fhccontent');
router.afterEach((to, from, failure) => {
// dont log cms admin preview
if (to.query.preview === '1')
return;
app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath));
});
-44
View File
@@ -1,44 +0,0 @@
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: [
{
// Only content_id is required. CmsAdmin fills the missing parts
// from the content itself and rewrites the address.
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');
+2 -10
View File
@@ -38,9 +38,7 @@ export default {
methods: {
fetchContent(){
this.$api
.call(ApiCms.content(
this.content_id_internal, this.version, this.sprache, this.sichtbar, this.preview
))
.call(ApiCms.content(this.content_id_internal, this.version, this.sprache, this.sichtbar))
.then(res => {
this.$nextTick(function() {
this.content = res.data.content;
@@ -59,14 +57,8 @@ export default {
}
},
computed: {
// The viewer language, unless the address names one. The CMS admin previews a
// chosen language, which is not always the language of the editor interface.
sprache(){
return this.$route?.query?.sprache || this.$p.user_language.value;
},
// A preview renders the content without counting a view.
preview(){
return this.$route?.query?.preview === '1';
return this.$p.user_language.value;
},
computeContentType: function () {
switch (this.content_type) {
@@ -1,5 +1,4 @@
import { replaceRelativeLegacyLink } from "../../../../helpers/LegacyLinkReplaceHelper.js"
import ContentcomponentHost from "../../../Contentcomponents/ContentcomponentHost.js";
export default {
name: "GeneralComponent",
props:{
@@ -7,13 +6,7 @@ export default {
type:String,
required:true,
},
content_id:{
type: [Number, String],
},
},
components: {
ContentcomponentHost
},
methods: {
sanitizeLegacyTables(table) {
@@ -131,7 +124,7 @@ export default {
template: /*html*/ `
<!-- div that contains the content -->
<div v-if="content" class="container" style="max-width: 100%;"><div class="row"><div class="col">
<contentcomponent-host :content="content" :content-id="content_id"></contentcomponent-host>
<div v-html="content" ></div>
</div></div></div>
<p v-else>Content was not found</p>
`,
@@ -134,7 +134,6 @@ export default {
template: /*html*/ `
<!-- div that contains the content -->
<div v-if="content" class="container" style="max-width: 100%;"><div class="row"><div class="col">
<!-- No contentcomponent-host on purpose: news contents stay static by convention. -->
<div v-html="content" ></div>
</div></div></div>
<p v-else>Content was not found</p>
@@ -1,4 +1,4 @@
import ContentcomponentHost from "../../../Contentcomponents/ContentcomponentHost.js";
export default {
name: "RaumComponent",
data() {
@@ -15,9 +15,6 @@ export default {
type: [Number, String],
}
},
components: {
ContentcomponentHost
},
methods: {
sanitizeLegacyTables(table) {
@@ -138,7 +135,7 @@ export default {
<!-- div that contains the content -->
<!-- TODO: test with more img content from cms-->
<div v-if="imgContent"><img v-bind="imgContent"/></div>
<contentcomponent-host v-else-if="content" :content="content" :content-id="content_id"></contentcomponent-host>
<div v-html="content" v-else-if="content" ></div>
<p v-else>Content was not found</p>
`,
};
-1
View File
@@ -117,7 +117,6 @@ export default {
></pagination>
<div class="container-fluid mt-4">
<div class="row">
<!-- No contentcomponent-host on purpose: news stays static by convention. -->
<div class="col" v-html="content">
</div>
<div class="col-auto">
+30 -19
View File
@@ -1,6 +1,7 @@
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiStudienSemester from '../../../api/factory/studiensemester.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
@@ -16,13 +17,12 @@ export default {
},
data() {
return {
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
isMitarbeiter: false,
isStudent: false,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
startSemester: null,
endSemester: null,
};
},
inject: ["isMobile"],
@@ -48,9 +48,12 @@ export default {
return this.propsViewData?.mode;
},
downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false;
const startDate = this.startSemester?.start;
const endDate = this.endSemester ? this.endSemester.ende : this.startSemester?.ende;
if (!startDate || !endDate || !this.uid)
return false;
let type = false;
type = this.isStudent ? 'student' : type;
type = this.isMitarbeiter ? 'lektor' : type;
@@ -61,10 +64,10 @@ export default {
const opts = { zone: this.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.fromISO(startDate, opts)
.toUnixInteger();
const ende = luxon.DateTime
.fromISO(this.studiensemester_ende, opts)
.fromISO(endDate, opts)
.toUnixInteger();
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
@@ -109,16 +112,21 @@ export default {
}
});
},
updateRange(rangeInterval) {
this.$api
.call(ApiLvPlan.studiensemesterDateInterval(
rangeInterval.end.startOf('week').toISODate()
))
.then(res => {
this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
async updateRange(rangeInterval) {
const semesterResponse = await this.$api.call(
ApiStudienSemester.getContainingOrNearestByDateRange(
rangeInterval.s.toISO().slice(0, 10),
rangeInterval.e.toISO().slice(0, 10),
),
);
this.startSemester =
semesterResponse.data.length
? semesterResponse.data[0]
: null;
this.endSemester =
semesterResponse.data.length > 1
? semesterResponse.data[1]
: null;
},
getPromiseFunc(start, end) {
return [
@@ -142,8 +150,11 @@ export default {
<div class="cis-lvplan-personal d-flex flex-column h-100">
<h2>
{{ $p.t('lehre/stundenplan') }}
<span v-if="studiensemester_kurzbz" class="ps-3">
{{ studiensemester_kurzbz }}
<span v-if="startSemester?.studiensemester_kurzbz">
{{ startSemester.studiensemester_kurzbz }}
<span v-if="endSemester?.studiensemester_kurzbz">
{{ "- " + endSemester.studiensemester_kurzbz }}
</span>
</span>
</h2>
<hr>
+32 -25
View File
@@ -5,6 +5,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from "../.././../api/factory/lvPlan.js";
import ApiOtherLvPlan from "../.././../api/factory/otherLvPlan.js";
import ApiAuthinfo from "../../../api/factory/authinfo.js";
import ApiStudienSemester from '../../../api/factory/studiensemester.js';
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
@@ -22,9 +23,6 @@ export default {
data() {
return {
localProps: {},
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
isOtherPersonMitarbeiter: false,
isOtherPersonStudent: false,
currentStgBezeichnung: null,
@@ -36,6 +34,8 @@ export default {
photo: "",
},
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
startSemester: null,
endSemester: null,
};
},
inject: ["isMobile"],
@@ -67,11 +67,10 @@ export default {
// do not show download links in otherLvPlan Mode
return false;
if (
!this.studiensemester_start ||
!this.studiensemester_ende ||
!this.propsViewData.otherUid
)
const startDate = this.startSemester?.start;
const endDate = this.endSemester ? this.endSemester.ende : this.startSemester?.ende;
if (!startDate || !endDate || !this.uid)
return false;
const type = this.isOtherPersonStudent
@@ -84,11 +83,11 @@ export default {
const opts = { zone: this.timezone };
const start = luxon.DateTime.fromISO(
this.studiensemester_start,
startDate,
opts,
).toUnixInteger();
const ende = luxon.DateTime.fromISO(
this.studiensemester_ende,
endDate,
opts,
).toUnixInteger();
@@ -166,19 +165,21 @@ export default {
},
});
},
updateRange(rangeInterval) {
this.$api
.call(
ApiLvPlan.studiensemesterDateInterval(
rangeInterval.end.startOf("week").toISODate(),
),
)
.then((res) => {
this.studiensemester_kurzbz =
res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
async updateRange(rangeInterval) {
const semesterResponse = await this.$api.call(
ApiStudienSemester.getContainingOrNearestByDateRange(
rangeInterval.s.toISO().slice(0, 10),
rangeInterval.e.toISO().slice(0, 10),
),
);
this.startSemester =
semesterResponse.data.length
? semesterResponse.data[0]
: null;
this.endSemester =
semesterResponse.data.length > 1
? semesterResponse.data[1]
: null;
},
getPromiseFunc(start, end) {
return [
@@ -250,8 +251,14 @@ export default {
<div class="d-flex flex-column h-100">
<h2>
<div class="d-flex flex-row justify-content-between align-items-center">
<span>
{{ $p.t('lehre/stundenplan') + (studiensemester_kurzbz ? " " + studiensemester_kurzbz : "") }}
<span>
{{ $p.t('lehre/stundenplan') }}
<span v-if="startSemester?.studiensemester_kurzbz">
{{ startSemester.studiensemester_kurzbz }}
<span v-if="endSemester?.studiensemester_kurzbz">
{{ "- " + endSemester.studiensemester_kurzbz }}
</span>
</span>
</span>
<div @click="this.$router.push({name: 'ProfilView', params: {uid: propsViewData.otherUid}})" type="button" class="d-flex flex-row align-items-center gap-3">
<span v-if="otherPersonData.fullNameWithTitle?.length">
+1 -1
View File
@@ -186,7 +186,7 @@ export default {
</div>
</div>
<nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
<nav id="nav-main" class="cis-main-menu offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
<div id="nav-main-sticky">
<div class="d-flex flex-row h-100">
<div class="offcanvas-body p-0">
+5 -1
View File
@@ -162,6 +162,7 @@ export default {
<div class="btn-group w-100">
<a :target="target"
:href="(entry.menu_open && hasFullLink) ? entry.url : null"
draggable="false"
@click="toggleCollapse"
:class="{
'btn btn-default rounded-0 text-start': true,
@@ -180,12 +181,15 @@ export default {
</div>
<ul ref="children"
class="nav w-100 collapse">
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeContent" v-for="child in entry.childs" :key="child" :entry="child" :level="level + 1"/>
<li>
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeContent" v-for="child in entry.childs" :key="child" :entry="child" :level="level + 1"/>
</li>
</ul>
</template>
<a v-else
:href="entry.url"
:target="target"
draggable="false"
:class="{
'btn btn-default rounded-0 w-100 text-start': true,
['btn-level-' + level]: true,
-303
View File
@@ -1,303 +0,0 @@
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 Hierarchy from './Tabs/Hierarchy.js';
import History from './Tabs/History.js';
import Delete from './Tabs/Delete.js';
import Clickstats from './Tabs/Clickstats.js';
import HorizontalSplit from '../horizontalsplit/horizontalsplit.js';
const TAB_COMPONENTS = {
properties: Properties,
content: ContentTab,
permissions: Permissions,
hierarchy: Hierarchy,
history: History,
delete: Delete
};
const TABS = [
{ key: 'properties', phrase: 'cms/tabEigenschaften' },
{ key: 'content', phrase: 'cms/tabInhalt' },
{ key: 'permissions', phrase: 'cms/tabRechte' },
{ key: 'hierarchy', phrase: 'cms/tabHierarchie' },
{ key: 'history', phrase: 'cms/tabHistory' },
{ key: 'delete', phrase: 'cms/tabLoeschen' }
];
// The view renders the clickstats attribute only for an admin, and only with the config
// flag set.
const CLICKSTATS_TAB = { key: 'clickstats', phrase: 'cms/tabKlickstatistik' };
export default {
name: 'CmsAdmin',
components: {
'cms-tree': ContentTree,
'cms-content-header': ContentHeader,
'horizontal-split': HorizontalSplit,
...TAB_COMPONENTS
},
props: {
cmsRoot: String,
authUid: String,
defaultLanguage: { type: String, default: 'German' },
clickstats: { type: Boolean, default: false }
},
data() {
return {
contentId: null,
sprache: null,
version: null,
tab: 'properties',
contentInfo: null,
pending: 0
};
},
computed: {
tabs() {
return this.clickstats ? TABS.concat([CLICKSTATS_TAB]) : TABS;
},
tabComponents() {
return this.clickstats
? { ...TAB_COMPONENTS, clickstats: Clickstats }
: TAB_COMPONENTS;
},
activeTabComponent() {
return this.tabComponents[this.tab] || null;
},
busy() {
return this.pending > 0;
}
},
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;
// unknown tab fallback
const tab = this.tabComponents[params.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) return;
if (this.contentInfo?.content_id !== id) {
this.loadContentInfo().then(() => { this.applyRouteDefaults(); });
} else {
this.applyRouteDefaults();
}
}
},
immediate: true
}
},
methods: {
loadContentInfo() {
if (this.contentId === null) {
this.contentInfo = null;
return Promise.resolve();
}
this.pending++;
return this.$api
.call(ApiCmsAdmin.getContent(this.contentId))
.then(result => {
this.contentInfo = result.data;
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.pending--; });
},
// Language, version and tab are optional in the address. Fill in what the address
// leaves out, and replace a value that the content does not have. Then write the
// complete address back, so a reload and a copied link both work.
applyRouteDefaults() {
if (this.contentId === null || !this.contentInfo) return;
const languages = this.contentInfo.languages || [];
const versions = this.contentInfo.versions || {};
let lang = this.sprache;
if (lang === null || !languages.includes(lang)) {
lang = languages.includes(this.defaultLanguage)
? this.defaultLanguage
: languages[0] || null;
}
const langVersions = (lang && versions[lang]) ? versions[lang] : [];
let ver = this.version;
if (ver === null || !langVersions.includes(ver)) {
ver = langVersions.length ? Math.max(...langVersions) : null;
}
if (lang === this.sprache && ver === this.version) return;
this.sprache = lang;
this.version = ver;
// Replace, or the short address stays in the history and the back button
// walks through both forms of the same page.
this.pushRoute(true);
},
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;
}
// Set id, language and version in one flush, or the tabs query the new content
// with the version of the old one.
this.pending++;
this.$api
.call(ApiCmsAdmin.getContent(content_id))
.then(result => {
this.contentId = content_id;
this.contentInfo = result.data;
this.preselectLanguageVersion();
this.pushRoute();
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.pending--; });
},
selectTab(tab) {
this.tab = tab;
this.pushRoute();
},
selectLanguageVersion(sprache, version) {
this.sprache = sprache;
this.version = version;
this.pushRoute();
},
// Three grades of tree update. Pick the cheapest one that stays correct.
// A full reload drops the expanded branches and refetches the filter meta,
// so keep it for changes that move nodes between branches.
reloadTree() {
if (this.$refs.tree) this.$refs.tree.reload();
},
refreshTreeNode(contentId) {
if (this.$refs.tree) this.$refs.tree.refreshNode(contentId);
},
patchTreeNode(payload) {
if (this.$refs.tree) this.$refs.tree.patchNode(payload.content_id, payload.patch);
},
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(replace = false) {
if (this.contentId === null) {
this.$router.push({ name: 'index' });
return;
}
// The optional parts are positional. Stop at the first missing one, or the
// address carries an empty segment.
const params = { content_id: this.contentId };
if (this.sprache !== null) {
params.sprache = this.sprache;
if (this.version !== null) {
params.version = this.version;
params.tab = this.tab;
}
}
const target = { name: 'content', params };
if (replace) {
this.$router.replace(target);
} else {
this.$router.push(target);
}
}
},
template: `
<div class="cms-admin h-100">
<horizontal-split :default-ratio="[25, 75]">
<template #left>
<cms-tree
ref="tree"
:active-content-id="contentId"
@select-content="selectContent"
></cms-tree>
</template>
<template #right>
<div class="cms-admin-main h-100 d-flex flex-column" v-if="contentId !== null">
<cms-content-header
:content-id="contentId"
:sprache="sprache"
:version="version"
:contentInfo="contentInfo"
@select-language-version="selectLanguageVersion"
@reload-content-info="loadContentInfo"
></cms-content-header>
<ul class="nav nav-tabs px-3">
<li class="nav-item" v-for="t in tabs" :key="t.key">
<a class="nav-link"
:class="{ active: tab === t.key }"
href="#"
@click.prevent="selectTab(t.key)"
>{{ $p.t(t.phrase) }}</a>
</li>
</ul>
<div class="cms-admin-tab-content flex-grow-1 overflow-auto">
<component
:is="activeTabComponent"
:content-id="contentId"
:sprache="sprache"
:version="version"
:contentInfo="contentInfo"
@reload-content-info="loadContentInfo"
@reload-tree="reloadTree"
@refresh-tree-node="refreshTreeNode"
@patch-tree-node="patchTreeNode"
@select-content="selectContent"
></component>
</div>
</div>
<div class="cms-admin-main h-100 d-flex align-items-center justify-content-center text-muted" v-else>
{{ $p.t('cms/keinEintragGewaehlt') }}
</div>
</template>
</horizontal-split>
<div v-if="busy" class="cms-admin-overlay">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{{ $p.t('ui/loading') }}</span>
</div>
</div>
</div>
`
};
@@ -1,381 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
import {
catalog,
getDescriptor,
CONTENTCOMPONENT_CLASS,
CONTENTCOMPONENT_ATTR
} from '../../Contentcomponents/catalog.js';
/**
* The insert and edit dialogs for a contentcomponent marker.
*
* Lives apart from FieldWysiwyg so that file keeps to the editor itself. Everything that
* knows about the catalog, the marker markup and the dialogs is here.
*/
// Helper fields a picker adds to a dialog. They must not collide with a prop name, and
// markerHtml ignores them because it walks the catalog, not the dialog data.
const FELD_KATEGORIE = '__kategorie';
const FELD_DOKUMENT = '__dokument';
// Filled in createContentcomponentDialog. The factory is no Vue component, so the
// phrases plugin is handed in instead of read from an instance.
let t = key => key;
// A function, not a constant: the text is translated when the dialog is built.
const leereAuswahl = () => ({ value: '', text: t('ccAuswaehlen') });
// A prop source fills a dialog field with a list from the server. The key is the
// "source" value in the catalog. A prop without a known source gets a plain text field.
const SOURCES = {
oe: {
request: () => ApiCmsAdmin.getOrganisationseinheiten(),
item: oe => ({
value: oe.oe_kurzbz,
text: (oe.bezeichnung || oe.oe_kurzbz)
+ (oe.aktiv ? '' : ' (' + t('inaktiv') + ')')
})
},
dmskategorie: {
request: () => ApiCmsAdmin.getDmsKategorien(),
item: kat => ({
value: kat.kategorie_kurzbz,
text: (kat.bezeichnung || kat.kategorie_kurzbz) + ' (' + kat.anzahl + ')'
})
},
// Surname first
person: {
request: () => ApiCmsAdmin.getMitarbeiter(),
item: person => ({
value: person.uid,
text: person.nachname + ' ' + person.vorname + ' (' + person.uid + ')'
})
}
};
function escapeAttr(value)
{
return String(value)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// Builds the marker. It stays empty: CIS4 renders the component into it, and every other
// renderer shows nothing. NOTE: if a marker disappears on save, check the TinyMCE
// serializer first. noneditable keeps the node, but an empty block is the exposed part.
function markerHtml(name, descriptor, data)
{
let attrs = '';
for (const propName of Object.keys(descriptor.props || {}))
{
const value = data[propName];
// false is an unchecked box. The absent attribute already means false.
if (value === '' || value === false || value === undefined || value === null)
continue;
attrs += ' data-' + propName + '="' + escapeAttr(value) + '"';
}
return '<div class="' + CONTENTCOMPONENT_CLASS + '" '
+ CONTENTCOMPONENT_ATTR + '="' + name + '"' + attrs + '></div>';
}
// Reads an existing marker back into dialog values.
function readMarker(node)
{
const name = node.getAttribute(CONTENTCOMPONENT_ATTR);
const descriptor = getDescriptor(name);
if (!descriptor)
return null;
const values = {};
for (const propName of Object.keys(descriptor.props || {}))
values[propName] = node.getAttribute('data-' + propName) || '';
return { name: name, values: values };
}
/**
* @param {object} options { api: the $api plugin, onChanged: called after a write }
*/
export function createContentcomponentDialog(options)
{
const api = options.api;
const onChanged = options.onChanged || function () {};
t = (key, args) => options.p.t('cms/' + key, args);
const sourceItems = {};
const sourceRequests = {};
// These lists are large and rarely needed. Load each one once, on demand.
function loadSource(key)
{
if (sourceRequests[key])
return sourceRequests[key];
const source = SOURCES[key];
sourceRequests[key] = api
.call(source.request())
.then(res => { sourceItems[key] = (res.data || []).map(source.item); })
.catch(() => { sourceItems[key] = []; });
return sourceRequests[key];
}
// Step one: pick the component. A second dialog then asks for its properties.
// Two dialogs avoid the redial logic a single changing dialog would need.
function openPicker(editor)
{
if (!catalog.length)
return;
editor.windowManager.open({
title: t('ccDialogTitel'),
initialData: { component: catalog[0].name },
body: {
type: 'panel',
items: [{
type: 'selectbox',
name: 'component',
label: t('ccContentcomponent'),
items: catalog.map(entry => ({
value: entry.name,
text: t(entry.labelPhrase)
}))
}]
},
buttons: [
{ type: 'cancel', text: t('abbrechen') },
{ type: 'submit', text: t('weiter'), primary: true }
],
onSubmit(dialog) {
const name = dialog.getData().component;
dialog.close();
openProperties(editor, name, null, null);
}
});
}
// Step two. node is null for an insert and the marker element for an edit.
function openProperties(editor, name, values, node)
{
const descriptor = getDescriptor(name);
if (!descriptor)
return;
const specs = Object.entries(descriptor.props || {});
const sources = specs
.map(([, spec]) => spec.source)
.filter((key, i, all) => SOURCES[key] && all.indexOf(key) === i);
// A document picker needs the category list even though no prop declares it.
if (specs.some(([, spec]) => spec.picker === 'dms') && sources.indexOf('dmskategorie') === -1)
sources.push('dmskategorie');
Promise.all(sources.map(loadSource)).then(() => {
// Documents of the category currently chosen in the dialog. A category change
// refills this and redials, because the items of a selectbox are fixed once
// the dialog is built.
let dokumente = [];
// A stored value that is no longer in the list, for example a person who left.
// Without this extra option the select would fall back to its first entry and
// silently replace the value on save. slice() keeps the cache clean.
function listeFuer(propName, spec)
{
const liste = (sourceItems[spec.source] || []).slice();
const wert = values && values[propName];
if (wert && !liste.some(eintrag => eintrag.value === wert))
liste.unshift({ value: wert, text: t('ccNichtInDerAuswahl', [wert]) });
return liste;
}
function itemsBauen()
{
return specs.reduce((alle, [propName, spec]) => {
if (SOURCES[spec.source])
{
alle.push({
type: 'selectbox',
name: propName,
label: spec.labelPhrase ? t(spec.labelPhrase) : propName,
items: listeFuer(propName, spec)
});
return alle;
}
if (spec.type === 'boolean')
{
alle.push({
type: 'checkbox',
name: propName,
label: spec.labelPhrase ? t(spec.labelPhrase) : propName
});
return alle;
}
alle.push({
type: 'input',
name: propName,
label: spec.labelPhrase ? t(spec.labelPhrase) : propName
});
if (spec.picker === 'dms')
{
alle.push({
type: 'selectbox',
name: propName + FELD_KATEGORIE,
label: t('ccKategorie'),
items: [leereAuswahl()].concat(sourceItems.dmskategorie || [])
});
alle.push({
type: 'selectbox',
name: propName + FELD_DOKUMENT,
label: t('ccDokumentHinzufuegen'),
items: [leereAuswahl()].concat(dokumente)
});
}
return alle;
}, []);
}
function konfigBauen(daten)
{
return {
title: t(descriptor.labelPhrase),
initialData: daten,
body: { type: 'panel', items: itemsBauen() },
onChange(dialog, details) {
aufAenderung(dialog, details.name);
},
buttons: [
{ type: 'cancel', text: t('abbrechen') },
{ type: 'submit', text: t('uebernehmen'), primary: true }
],
onSubmit(dialog) {
const html = markerHtml(name, descriptor, dialog.getData());
dialog.close();
if (node)
editor.dom.setOuterHTML(node, html);
else
editor.insertContent(html);
onChanged();
}
};
}
function aufAenderung(dialog, feld)
{
// A new category means a new document list, and only redial can replace
// the options of a selectbox.
if (feld.endsWith(FELD_KATEGORIE))
{
const ziel = feld.slice(0, -FELD_KATEGORIE.length);
const daten = dialog.getData();
const kategorie = daten[feld];
daten[ziel + FELD_DOKUMENT] = '';
if (!kategorie)
{
dokumente = [];
dialog.redial(konfigBauen(daten));
return;
}
api
.call(ApiCmsAdmin.getDmsKategorieDokumente(kategorie))
.then(res => {
dokumente = (res.data || []).map(dokument => ({
value: String(dokument.dms_id),
// A document outside the CIS search still renders here, so
// the editor has to see which one that is.
text: dokument.cis_suche
? dokument.name
: t('ccNichtInCisSuche', [dokument.name])
}));
})
.catch(() => { dokumente = []; })
.then(() => { dialog.redial(konfigBauen(daten)); });
return;
}
// Choosing a document appends it and clears the select, so the next one can
// be picked straight away. The reset fires this handler again with an empty
// value, which the guard below drops.
if (feld.endsWith(FELD_DOKUMENT))
{
const ziel = feld.slice(0, -FELD_DOKUMENT.length);
const daten = dialog.getData();
const wert = daten[feld];
if (!wert)
return;
const ids = String(daten[ziel] || '')
.split(',')
.map(teil => teil.trim())
.filter(teil => teil);
if (ids.indexOf(wert) === -1)
ids.push(wert);
const patch = {};
patch[ziel] = ids.join(', ');
patch[feld] = '';
dialog.setData(patch);
}
}
// Every field needs a start value, otherwise the dialog reports it as unknown.
const startdaten = {};
for (const [propName, spec] of specs)
{
if (spec.type === 'boolean')
{
startdaten[propName] = !!values && values[propName] === 'true';
continue;
}
const liste = sourceItems[spec.source];
startdaten[propName] = (values && values[propName])
|| (liste && liste.length ? liste[0].value : '');
if (spec.picker === 'dms')
{
startdaten[propName + FELD_KATEGORIE] = '';
startdaten[propName + FELD_DOKUMENT] = '';
}
}
editor.windowManager.open(konfigBauen(startdaten));
});
}
// Opens the property dialog of an existing marker.
function openMarker(editor, node)
{
const marker = readMarker(node);
if (marker)
openProperties(editor, marker.name, marker.values, node);
}
return {
openPicker: openPicker,
openMarker: openMarker
};
}
@@ -1,25 +0,0 @@
export default {
name: 'FieldString',
props: {
modelValue: { type: String, default: '' },
label: String,
required: Boolean,
disabled: Boolean,
type: { type: String, default: 'text' }
},
emits: ['update:modelValue'],
template: `
<div class="mb-3">
<label class="form-label">
{{ label }}<span v-if="required" class="text-danger"> *</span>
</label>
<input
class="form-control"
:type="type"
:value="modelValue"
:disabled="disabled"
@input="$emit('update:modelValue', $event.target.value)"
>
</div>
`
};
@@ -1,162 +0,0 @@
import { CONTENTCOMPONENT_CLASS, CONTENTCOMPONENT_ATTR } from '../../Contentcomponents/catalog.js';
import { createContentcomponentDialog } from './ContentcomponentDialog.js';
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;
},
// Opens the legacy DMS browser and resolves with the relative URL it posts back.
// Used for images and links, where the editor needs the file preview of that
// browser. The contentcomponent dialog picks documents from a list instead, without
// leaving the dialog. The bridge in cms/tinymce_dms.php sends { typ, url } to
// window.opener and closes itself.
openDmsBrowser() {
const vm = this;
return new Promise(resolve => {
if (vm._dmsListener)
window.removeEventListener('message', vm._dmsListener);
const url = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cms/tinymce_dms.php';
// The window name selects the bridge path in cms/tinymce_dms.php and survives
// its internal navigation. The legacy admin opens the same page as 'DMS'.
window.open(url, 'DMS_CIS4', '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;
window.removeEventListener('message', onMessage);
vm._dmsListener = null;
resolve(event.data.url);
};
vm._dmsListener = onMessage;
window.addEventListener('message', onMessage);
});
}
},
watch: {
modelValue(newVal) {
if (this.editor && this.editor.getContent() !== newVal) {
this.editor.setContent(newVal || '');
}
}
},
mounted() {
const vm = this;
// The dialogs live in their own module. This file stays with the editor itself.
this.contentcomponentDialog = createContentcomponentDialog({
api: this.$api,
// The dialog module is no component, so it cannot reach the phrases itself.
p: this.$p,
onChanged: () => vm.$emit('update:modelValue', vm.editor.getContent())
});
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: '*[*]',
// A contentcomponent marker is one atomic block. The editor must not type into it.
noneditable_noneditable_class: CONTENTCOMPONENT_CLASS,
// The marker renders nothing here, so the editor has to show that it is there.
content_style: '.' + CONTENTCOMPONENT_CLASS + '{'
+ 'display:block;margin:.5rem 0;padding:.5rem;min-height:2.5rem;'
+ 'border:1px dashed #7a7a7a;background:#f2f2f2;cursor:pointer;}'
+ '.' + CONTENTCOMPONENT_CLASS + '::before{'
+ 'content:"Contentcomponent: " attr(' + CONTENTCOMPONENT_ATTR + ');'
+ 'font:12px monospace;color:#555;}',
forced_root_block: '',
// No style_formats, and no styleselect in the toolbar below. The six formats
// of the legacy admin are gone, and none of them earned a port.
//
// Three wrote a div, while every stylesheet targets a td: skin/styles/tw.css
// declares td.cmscontent, td.menubox and td.teambox. The live content agrees,
// it carries those classes on a td about 1400 times and on a div twice.
// Tablesorter wrote tablesorter_col_0, which no file in this repository
// reads, and skin/style.css.php never serves skin/tablesort.css.
//
// cmstable and screenshot_boxshadow did work, but only in the legacy
// renderer. public/css/Cis4/Cms.css binds its copies to #cms, and CIS4
// renders into #fhc-cms-content. CIS4 is the target, so they went too.
//
// A styling option for editors is a feature of its own. See
// docs/cms-darstellung-entwurf.md.
plugins: 'code lists link image table fullscreen paste searchreplace charmap directionality visualblocks anchor hr noneditable',
toolbar: [
'code | contentcomponent | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | 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'
],
// 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.
file_picker_callback: (callback, value, meta) => {
vm.openDmsBrowser().then(callback);
},
setup: (editor) => {
vm.editor = editor;
editor.ui.registry.addButton('contentcomponent', {
text: vm.$p.t('cms/ccContentcomponent'),
tooltip: vm.$p.t('cms/ccDialogTitel'),
onAction: () => vm.contentcomponentDialog.openPicker(editor)
});
// A double click on a marker reopens its property dialog.
editor.on('dblclick', (event) => {
const node = editor.dom.getParent(event.target, '.' + CONTENTCOMPONENT_CLASS);
if (node)
vm.contentcomponentDialog.openMarker(editor, node);
});
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: `
<div class="mb-3">
<label class="form-label">
{{ label }}<span v-if="required" class="text-danger"> *</span>
</label>
<textarea ref="editorArea" :id="editorId"></textarea>
</div>
`
};
-72
View File
@@ -1,72 +0,0 @@
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: `
<div v-if="schema && schema.fields">
<template v-for="field in schema.fields" :key="field.name">
<field-wysiwyg
v-if="field.type === 'wysiwyg'"
:ref="'wysiwyg-' + field.name"
:modelValue="modelValue[field.name] || ''"
:label="field.name"
:required="field.required"
:disabled="disabled"
@update:modelValue="updateField(field.name, $event)"
></field-wysiwyg>
<div v-else-if="field.type === 'boolean'" class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
:checked="!!modelValue[field.name]"
:disabled="disabled"
@change="updateField(field.name, $event.target.checked)"
:id="'field-' + field.name"
>
<label class="form-check-label" :for="'field-' + field.name">
{{ field.name }}<span v-if="field.required" class="text-danger"> *</span>
</label>
</div>
</div>
<field-string
v-else
:modelValue="modelValue[field.name] || ''"
:label="field.name"
:required="field.required"
:disabled="disabled"
:type="field.type === 'date' ? 'date' : 'text'"
@update:modelValue="updateField(field.name, $event)"
></field-string>
</template>
</div>
`
};
@@ -1,180 +0,0 @@
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'],
data() {
return {
titel: '',
allSprachen: [],
versionDetails: [],
// Target of the translation dropdown, used once the list grows past a pair.
neueSprache: ''
};
},
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.alertSuccess(this.$p.t('cms/neueVersionAngelegt'));
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(newSprache) {
if (!newSprache) return;
this.neueSprache = '';
this.$api
.call(ApiCmsAdmin.postTranslation(
this.contentId, this.sprache, this.version, newSprache
))
.then(() => {
this.$fhcAlert.alertSuccess(this.$p.t('cms/uebersetzungAngelegt'));
this.$emit('reload-content-info');
this.$emit('select-language-version', newSprache, this.version);
})
.catch(this.$fhcAlert.handleSystemError);
}
},
mounted() {
this.loadSprachen();
this.loadTitel();
this.loadVersionDetails();
},
template: `
<div class="px-3 pt-2 pb-2 border-bottom" v-if="contentInfo">
<div class="mb-2 fw-bold">
{{ $p.t('cms/contentId') }}: {{ contentId }}
| {{ $p.t('cms/version') }}: {{ version }}
| {{ $p.t('cms/sprache') }}: {{ sprache }}
| {{ $p.t('cms/titel') }}: {{ titel }}
</div>
<div class="d-flex align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<version-select
:versions="versions"
:version-details="versionDetails"
:version="version"
@select-version="selectVersion"
></version-select>
<sprache-select
:languages="languages"
:sprache="sprache"
@select-language="selectLanguage"
></sprache-select>
</div>
<div>
<button class="btn btn-sm btn-outline-primary"
@click="createVersion"
>{{ $p.t('cms/neueVersionAnlegen') }}</button>
</div>
<div class="d-flex align-items-center gap-1">
<!-- One button per missing language is fine for a pair. An institution
with more content languages would get a wall of them, each
carrying the full label. -->
<template v-if="availableSprachen.length > 2">
<select class="form-select form-select-sm w-auto"
v-model="neueSprache">
<option value="">{{ $p.t('cms/spracheWaehlen') }}</option>
<option v-for="s in availableSprachen" :key="s.sprache"
:value="s.sprache">{{ s.sprache }}</option>
</select>
<button class="btn btn-sm btn-outline-primary"
:disabled="!neueSprache"
@click="createTranslation(neueSprache)"
>{{ $p.t('cms/uebersetzungAnlegen') }}</button>
</template>
<template v-else>
<button v-for="s in availableSprachen" :key="s.sprache"
class="btn btn-sm btn-outline-primary"
@click="createTranslation(s.sprache)"
>{{ $p.t('cms/uebersetzungAnlegenIn', [s.sprache]) }}</button>
</template>
</div>
</div>
</div>
`
};
@@ -1,36 +0,0 @@
export default {
name: 'SpracheSelect',
props: {
languages: { type: Array, default: () => [] },
sprache: String
},
emits: ['select-language'],
computed: {
// Two languages read faster as a pair of buttons, and that is the case here. Other
// institutions run this software with more content languages, and there the row of
// buttons pushes the rest of the header off the line.
alsAuswahl() {
return this.languages.length > 2;
}
},
template: `
<select v-if="alsAuswahl"
class="form-select form-select-sm w-auto cms-sprache-select"
:title="$p.t('cms/sprache')"
:value="sprache"
@change="$emit('select-language', $event.target.value)"
>
<option v-for="lang in languages" :key="lang" :value="lang">{{ lang }}</option>
</select>
<div v-else class="btn-group btn-group-sm cms-sprache-select" role="group">
<button
v-for="lang in languages"
:key="lang"
type="button"
class="btn"
:class="lang === sprache ? 'btn-primary' : 'btn-outline-secondary'"
@click="$emit('select-language', lang)"
>{{ lang }}</button>
</div>
`
};
@@ -1,81 +0,0 @@
// Versions shown on each side of the open one.
const WINDOW = 3;
export default {
name: 'VersionSelect',
props: {
versions: { type: Array, default: () => [] },
versionDetails: { type: Array, default: () => [] },
version: Number
},
emits: ['select-version'],
computed: {
// A content can carry dozens of versions, and one button each breaks the header.
// Show the open version with three neighbours on each side. The first and the last
// stay reachable in one click, and a gap marks what the window leaves out.
// Below the threshold the window would hide nothing, so show every version.
items() {
const all = this.versions;
const active = all.indexOf(this.version);
if (active === -1 || all.length <= WINDOW * 2 + 3)
return all.map(value => ({ type: 'version', value }));
const from = Math.max(0, active - WINDOW);
const to = Math.min(all.length - 1, active + WINDOW);
const items = [];
if (from > 0) {
items.push({ type: 'version', value: all[0] });
// One version between the first and the window needs no gap marker.
if (from > 1) items.push({ type: 'gap', count: from - 1 });
}
for (let i = from; i <= to; i++)
items.push({ type: 'version', value: all[i] });
if (to < all.length - 1) {
if (to < all.length - 2)
items.push({ type: 'gap', count: all.length - 2 - to });
items.push({ type: 'version', value: all[all.length - 1] });
}
return items;
},
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: `
<div class="btn-group btn-group-sm" role="group">
<template v-for="(item, idx) in items" :key="item.type + idx">
<button
v-if="item.type === 'version'"
type="button"
class="btn"
:class="[
item.value === version ? 'btn-primary' : 'btn-outline-secondary',
{ 'opacity-50': !isSichtbar(item.value) }
]"
@click="$emit('select-version', item.value)"
>{{ item.value }}</button>
<button
v-else
type="button"
class="btn btn-outline-secondary"
disabled
:title="$p.t('cms/weitereVersionen', [item.count])"
>&hellip;</button>
</template>
</div>
`
};
-308
View File
@@ -1,308 +0,0 @@
// Safety net for the spinner.
const CIS4_LOAD_TIMEOUT = 10000;
// Cis.js reads window.innerWidth and switches to its mobile layout below 767px. In a frame
// that is the width of the frame, so these widths drive the real layout and not a zoom.
// Its resize listener reacts on its own, which means no reload and no second long wait.
const CIS4_WIDTHS = {
mobile: 375,
desktop: null
};
export default {
name: 'CmsPreview',
props: {
contentId: Number,
sprache: String,
version: Number,
templateKurzbz: String,
// The legacy renderer runs no Vue, so it draws nothing for a contentcomponent
// marker. The CIS4 view mounts them, which is the whole point of switching.
hasContentcomponents: Boolean,
// False for a template whose xslt_xhtml_c4 is a copy of the legacy stylesheet.
// Such a template emits a whole HTML document, and the CIS4 renderer writes that
// into a div, so the layout is lost. Defaults true, so an older payload warns
// about nothing.
hasCis4Stylesheet: { type: Boolean, default: true },
sichtbar: Boolean
},
data() {
// The legacy renderer stays the default, so nothing changes until an editor asks
// for the other one. The choice survives a page switch, like the menu switch.
const stored = localStorage.getItem('cms/previewMode');
const mode = (stored === 'legacy' || stored === 'cis4') ? stored : 'legacy';
const width = localStorage.getItem('cms/previewWidth');
return {
mode: mode,
// True while the split handle is dragged. See onSplitterDown.
dragging: false,
cis4Width: CIS4_WIDTHS.hasOwnProperty(width) ? width : 'desktop',
// The CIS4 shell pulls its whole stack before it shows anything, which takes a
// while. Start covered when the frame is the one on screen.
cis4Loading: mode === 'cis4'
};
},
watch: {
mode(value) {
if (value === 'cis4') this.startCis4Load();
else this.stopCis4Poll();
},
// A new content, language or version reloads the frame.
cis4PreviewUrl() {
if (this.mode === 'cis4') this.startCis4Load();
}
},
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 second preview renders the content the way CIS4 does: the route loads the
// CIS4 shell, which brings its stylesheets, its Vue app and the contentcomponents.
// CISVUE-Header sets the body class in-frame by itself, and Cis.css hides the CIS4
// header for it, so the frame shows the content and no navigation.
// version travels as a route segment, because Cms/Content.js already declares that
// prop and the route passes props. sprache and preview travel as query parameters,
// because neither is a prop there.
cis4PreviewUrl() {
let url = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ '/CisVue/Cms/content/' + this.contentId;
if (this.version !== null)
url += '/' + this.version;
url += '?preview=1';
if (this.sprache)
url += '&sprache=' + encodeURIComponent(this.sprache);
return url;
},
// The overlay sits on the wrapper, so the wrapper carries the width. Otherwise the
// spinner would cover the whole pane while the frame is only 375px wide.
cis4WrapStyle() {
const px = CIS4_WIDTHS[this.cis4Width];
// content-box on purpose: the frame reads its own window.innerWidth, and with
// the usual border-box the border would eat two pixels. The preset is meant
// to be the width the CIS4 app sees, so it must not depend on the frame.
return px === null
? ''
: 'width: ' + px + 'px; box-sizing: content-box; max-width: 100%;'
+ ' margin: 0 auto;';
},
// 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: {
selectMode(value) {
localStorage.setItem('cms/previewMode', value);
this.mode = value;
},
// The split handle sits right next to the frame. As soon as the pointer crosses
// into the frame, the frame document takes the mousemove and the mouseup, the
// listeners HorizontalSplit put on the parent window never fire, and the drag
// stops halfway. Turning pointer events off in the frame for the length of the
// drag lets those events through to the page underneath.
//
// Capture phase on purpose: HorizontalSplit.dragStart calls stopPropagation, so a
// listener that waits for the event to bubble back up never sees it at all.
onSplitterDown(event) {
const target = event.target;
if (target && target.closest && target.closest('.horizontalsplitter'))
this.dragging = true;
},
onSplitterUp() {
this.dragging = false;
},
selectWidth(value) {
localStorage.setItem('cms/previewWidth', value);
this.cis4Width = value;
// A frame resized by its parent gets no resize event of its own, so Cis.js
// would keep the window width it read at load and stay on that layout. Same
// origin, so tell the frame. That spares a reload of the whole CIS4 stack.
this.$nextTick(() => {
try {
const frame = this.$refs.cis4Frame;
const win = frame && frame.contentWindow;
if (win) win.dispatchEvent(new win.Event('resize'));
} catch (e) {
// Not reachable. The frame keeps the layout it loaded with.
}
});
},
startCis4Load() {
this.stopCis4Poll();
this.cis4Loading = true;
},
// The frame fires load once the CIS4 shell is there, but the shell then fetches the
// content over the API, so the load event alone clears the spinner too early and
// leaves the placeholder text on screen. The frame is same origin, so wait for the
// element the CIS4 renderer writes once it holds the content.
onCis4Load() {
this.stopCis4Poll();
const started = Date.now();
this.cis4Poll = setInterval(() => {
let done = false;
try {
const doc = this.$refs.cis4Frame && this.$refs.cis4Frame.contentDocument;
done = !!(doc && doc.getElementById('fhc-cms-content'));
} catch (e) {
// Not readable after all. Stop waiting rather than spin forever.
done = true;
}
if (done || Date.now() - started > CIS4_LOAD_TIMEOUT) {
this.stopCis4Poll();
this.cis4Loading = false;
}
}, 120);
},
stopCis4Poll() {
if (this.cis4Poll) {
clearInterval(this.cis4Poll);
this.cis4Poll = null;
}
},
neuLaden() {
if (this.$refs.previewFrame) {
this.$refs.previewFrame.src = this.previewUrl + '&t=' + Date.now();
}
if (this.$refs.cis4Frame) {
this.startCis4Load();
this.$refs.cis4Frame.src = this.cis4PreviewUrl + '&t=' + Date.now();
}
}
},
mounted() {
// On the window, not the document: HorizontalSplit ends the drag from a listener
// it puts on the window, and a mouseup aimed at the window never reaches a
// listener on the document. The window is the first node of the capture path, so
// it sees everything the document would and that case as well.
window.addEventListener('mousedown', this.onSplitterDown, true);
window.addEventListener('mouseup', this.onSplitterUp, true);
// Backstop. A missed mouseup would leave the frame unclickable.
window.addEventListener('blur', this.onSplitterUp);
},
beforeUnmount() {
window.removeEventListener('mousedown', this.onSplitterDown, true);
window.removeEventListener('mouseup', this.onSplitterUp, true);
window.removeEventListener('blur', this.onSplitterUp);
this.stopCis4Poll();
},
template: `
<div class="mt-3" :class="{ 'cms-preview-dragging': dragging }">
<div v-if="sichtbar === false" class="text-danger fw-bold mb-2">
{{ $p.t('cms/unsichtbarImLivesystem') }}
</div>
<div class="mb-2">
<a :href="cis4Url" target="_blank">{{ $p.t('cms/inCis4Ansehen') }}</a>
</div>
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn"
:class="mode === 'legacy' ? 'btn-primary' : 'btn-outline-secondary'"
@click="selectMode('legacy')"
>{{ $p.t('cms/vorschauLegacy') }}</button>
<button type="button" class="btn"
:class="mode === 'cis4' ? 'btn-primary' : 'btn-outline-secondary'"
@click="selectMode('cis4')"
>{{ $p.t('cms/vorschauCis4') }}</button>
</div>
<div v-if="mode === 'cis4' && !isRedirect"
class="btn-group btn-group-sm" role="group">
<button type="button" class="btn"
:class="cis4Width === 'desktop' ? 'btn-primary' : 'btn-outline-secondary'"
:title="$p.t('cms/breiteDesktop')"
@click="selectWidth('desktop')"
><i class="fa-solid fa-display"></i></button>
<button type="button" class="btn"
:class="cis4Width === 'mobile' ? 'btn-primary' : 'btn-outline-secondary'"
:title="$p.t('cms/breiteMobil')"
@click="selectWidth('mobile')"
><i class="fa-solid fa-mobile-screen-button"></i></button>
</div>
</div>
<template v-if="mode === 'legacy'">
<div v-if="hasContentcomponents" class="alert alert-info py-2">
{{ $p.t('cms/contentcomponentsNurCis4') }}
</div>
<template v-if="isRedirect">
<a :href="previewUrl" target="_blank">
{{ $p.t('cms/vorschauInEigenemFenster') }}
</a>
</template>
<template v-else>
<iframe
ref="previewFrame"
:src="previewUrl"
style="width: 100%; min-height: 500px; border: 1px solid #ccc;"
></iframe>
</template>
</template>
<template v-else>
<div v-if="!hasCis4Stylesheet" class="alert alert-warning py-2">
{{ $p.t('cms/keinCis4Stylesheet') }}
</div>
<template v-if="isRedirect">
<a :href="cis4PreviewUrl" target="_blank">
{{ $p.t('cms/vorschauInEigenemFenster') }}
</a>
</template>
<template v-else>
<div class="position-relative border" :style="cis4WrapStyle">
<iframe
ref="cis4Frame"
:src="cis4PreviewUrl"
@load="onCis4Load"
style="width: 100%; min-height: 500px; border: 0;
display: block;"
></iframe>
<div v-if="cis4Loading"
class="position-absolute top-0 start-0 w-100 h-100 d-flex
flex-column align-items-center justify-content-center"
style="background: rgba(255, 255, 255, 0.85);">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{{ $p.t('ui/loading') }}</span>
</div>
<div class="mt-2 text-muted small">
{{ $p.t('cms/vorschauLaedt') }}
</div>
</div>
</div>
</template>
</template>
</div>
`
};
-153
View File
@@ -1,153 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsClickstats',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['select-content'],
data() {
return {
own: null,
ranked: [],
// The ranking stays absent until the editor asks for it, so an empty table and
// a table that was never fetched stay apart.
rankingLoaded: false,
since: null,
months: 12,
loadingOwn: false,
loadingRanking: false
};
},
watch: {
contentId() { this.loadOwn(); },
sprache() { this.loadOwn(); }
},
methods: {
// One counting query. Cheap enough to run whenever the tab or the period opens.
loadOwn() {
if (this.contentId == null) return;
this.loadingOwn = true;
this.$api
.call(ApiCmsAdmin.getClickCount(this.contentId, this.months))
.then(result => {
this.own = result.data.own;
this.since = result.data.since;
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.loadingOwn = false; });
},
// Groups the whole log, which takes seconds. Only on request.
loadRanking() {
this.loadingRanking = true;
this.$api
.call(ApiCmsAdmin.getClickRanking(this.months, this.sprache))
.then(result => {
this.ranked = result.data.ranked || [];
this.since = result.data.since;
this.rankingLoaded = true;
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.loadingRanking = false; });
},
// A new period always refreshes the cheap number. The ranking follows only once it
// is on screen, so changing the period never costs the long wait unasked.
onPeriodChange() {
this.loadOwn();
if (this.rankingLoaded) this.loadRanking();
},
// since arrives as an SQL timestamp. Format it the way the other tabs do.
formatDate(value) {
if (!value) return '';
const dt = luxon.DateTime.fromSQL(value);
return dt.isValid ? dt.toFormat('dd.MM.yyyy') : value;
},
isCurrent(row) {
return row.content_id === this.contentId;
},
onContentClick(content_id) {
this.$emit('select-content', content_id);
}
},
created() {
this.monthOptions = [3, 6, 12, 24, 0];
},
mounted() {
this.loadOwn();
},
template: `
<div class="p-3" v-if="contentId != null">
<div class="d-flex gap-2 align-items-center mb-3">
<label class="form-label mb-0">{{ $p.t('cms/zeitraum') }}</label>
<select class="form-select form-select-sm"
style="max-width: 220px"
v-model.number="months"
:disabled="loadingOwn || loadingRanking"
@change="onPeriodChange">
<option v-for="m in monthOptions" :key="m" :value="m">
{{ m ? $p.t('cms/letzteNMonate', [m]) : $p.t('cms/gesamterZeitraum') }}
</option>
</select>
<i v-if="loadingOwn" class="fa-solid fa-spinner fa-spin"></i>
</div>
<div class="form-text mb-3">
{{ since
? $p.t('cms/klicksSeit', [formatDate(since)])
: $p.t('cms/klicksGesamt') }}
</div>
<div class="mb-4">
<h5>{{ $p.t('cms/aufrufeDiesesEintrags') }}</h5>
<span class="fs-4">{{ own === null ? '-' : own }}</span>
</div>
<h5>{{ $p.t('cms/meistAufgerufeneEintraege') }}</h5>
<button v-if="!rankingLoaded"
class="btn btn-outline-primary"
:disabled="loadingRanking"
@click="loadRanking">
<i v-if="loadingRanking" class="fa-solid fa-spinner fa-spin me-1"></i>
{{ $p.t('cms/alleKlickstatistikenLaden') }}
</button>
<table v-else-if="ranked.length > 0" class="table table-sm table-striped">
<thead>
<tr>
<th>{{ $p.t('cms/rang') }}</th>
<th>ID</th>
<th>{{ $p.t('cms/titel') }}</th>
<th>{{ $p.t('cms/aufrufe') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in ranked" :key="row.content_id"
:class="isCurrent(row) ? 'table-primary' : ''">
<td>{{ idx + 1 }}</td>
<td>{{ row.content_id }}</td>
<td>
<a href="#" @click.prevent="onContentClick(row.content_id)">
{{ row.titel || $p.t('cms/ohneTitel') }}
</a>
</td>
<td>{{ row.hits }}</td>
</tr>
</tbody>
</table>
<div v-else class="text-muted">
{{ $p.t('cms/keineAufrufeImZeitraum') }}
</div>
</div>
`
};
-199
View File
@@ -1,199 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
import XsdForm from '../Form/XsdForm.js';
import CmsPreview from '../Preview.js';
import { CONTENTCOMPONENT_ATTR } from '../../Contentcomponents/catalog.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', 'refresh-tree-node', 'select-content'],
data() {
return {
schema: null,
values: {},
sperre: null,
sichtbar: true,
loading: false,
saving: false
};
},
computed: {
// The legacy renderer runs no Vue and draws nothing for a marker. The preview says
// so, but only while the legacy view is the one on screen.
hasContentcomponents() {
return Object.values(this.values || {}).some(
value => typeof value === 'string' && value.indexOf(CONTENTCOMPONENT_ATTR) !== -1
);
},
// The redirect template carries a URL, and its preview opens in a new window
// instead of an iframe. So without the lock nothing on this tab shows the values.
// Read only fields spare the editor a lock that blocks the content for 24 hours.
isRedirect() {
return this.contentInfo?.template_kurzbz === 'redirect';
},
lockState() {
if (!this.sperre || this.sperre.gesperrt_uid === null) return 'free';
if (this.sperre.own) return 'own';
if (this.sperre.expired) return 'expired';
return 'foreign';
},
lockInfo() {
if (!this.sperre || this.sperre.gesperrt_uid === null) return '';
const start = this.formatDateTime(this.sperre.start);
const ende = this.formatDateTime(this.sperre.expires);
if (this.sperre.own)
return this.$p.t('cms/eigeneSperre', [start, ende]);
const uid = this.sperre.gesperrt_uid;
if (this.sperre.expired)
return this.$p.t('cms/gesperrtVonSeitAbgelaufen', [uid, start, ende]);
return this.$p.t('cms/gesperrtVonSeit', [uid, start, ende]);
}
},
watch: {
contentId() { this.loadFormData(); },
sprache() { this.loadFormData(); },
version() { this.loadFormData(); }
},
methods: {
formatDateTime(dateStr) {
if (!dateStr) return this.$p.t('cms/unbekannt');
const dt = luxon.DateTime.fromSQL(dateStr);
return dt.isValid ? dt.toFormat('dd.MM.yyyy HH:mm') : dateStr;
},
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);
},
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'));
// The form holds the title, and the tree shows it.
this.$emit('refresh-tree-node', this.contentId);
if (this.$refs.preview) this.$refs.preview.neuLaden();
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.saving = false; });
}
},
mounted() {
this.loadFormData();
},
template: `
<div class="p-3" v-if="contentId != null">
<template v-if="!loading && sperre">
<div v-if="sperre.gesperrt_uid !== null"
class="alert"
:class="lockState === 'expired' ? 'alert-warning' : 'alert-info'">
{{ lockInfo }}
</div>
<template v-if="lockState === 'free'">
<button class="btn btn-primary mb-3" @click="lock">
{{ $p.t('cms/zurBearbeitungSperren') }}
</button>
</template>
<template v-else-if="lockState === 'own'">
<button class="btn btn-outline-secondary mb-3" @click="unlock">
{{ $p.t('cms/sperreFreigeben') }}
</button>
<xsd-form
ref="xsdForm"
:schema="schema"
v-model="values"
:disabled="false"
></xsd-form>
<button class="btn btn-primary mt-2 mb-3" @click="save" :disabled="saving">
{{ $p.t('cms/speichern') }}
</button>
</template>
<template v-else-if="lockState === 'expired'">
<button class="btn btn-warning mb-3" @click="lock">
{{ $p.t('cms/sperreUebernehmen') }}
</button>
</template>
<template v-else>
<button v-if="sperre.may_force" class="btn btn-danger mb-3" @click="forceUnlock">
{{ $p.t('cms/freigabeErzwingen') }}
</button>
</template>
<xsd-form
v-if="isRedirect && lockState !== 'own'"
:schema="schema"
:modelValue="values"
:disabled="true"
></xsd-form>
<cms-preview
ref="preview"
:content-id="contentId"
:sprache="sprache"
:version="version"
:template-kurzbz="contentInfo?.template_kurzbz"
:has-cis4-stylesheet="contentInfo?.has_cis4_stylesheet !== false"
:has-contentcomponents="hasContentcomponents"
:sichtbar="sichtbar"
></cms-preview>
</template>
</div>
`
};
-154
View File
@@ -1,154 +0,0 @@
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', 'refresh-tree-node', 'select-content'],
data() {
return {
usages: [],
loadingUsage: true
};
},
computed: {
hasRestrict() {
return this.usages.some(u => u.table !== 'campus.tbl_infoscreen_content');
},
// 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.$fhcAlert.alertSuccess(this.$p.t('cms/versionGeloescht'));
this.$emit('reload-content-info');
// The tree shows the title of the newest version, so the label can change.
this.$emit('refresh-tree-node', this.contentId);
}).catch(this.$fhcAlert.handleSystemError);
},
deleteContent() {
this.$fhcAlert.confirm({
message: this.$p.t('cms/contentLoeschenBestaetigen', [
this.contentInfo.languages.length,
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.$fhcAlert.alertSuccess(this.$p.t('cms/contentGeloescht'));
// Children of the deleted entry can reappear under another parent, so the
// whole tree has to be rebuilt here.
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: `
<div class="p-3" v-if="contentId != null">
<h5>{{ $p.t('cms/dieseVersionLoeschen') }}</h5>
<div v-if="isLastVersion" class="alert alert-warning mb-3">
{{ $p.t('cms/letzteVersionNichtLoeschbar') }}
</div>
<div v-else class="mb-3">
<button class="btn btn-outline-danger" @click="deleteVersion">
{{ $p.t('cms/dieseVersionLoeschen') }}
</button>
</div>
<hr>
<h5>{{ $p.t('cms/ganzenContentLoeschen') }}</h5>
<div v-if="loadingUsage" class="mb-3">
<div class="spinner-border spinner-border-sm" role="status"></div>
</div>
<template v-else>
<div v-if="usages.length === 0" class="alert alert-info mb-3">
{{ $p.t('cms/keineVerwendungGefunden') }}
</div>
<div v-else class="mb-3">
<div class="alert alert-warning">
{{ $p.t('cms/contentWirdVerwendetIn') }}
</div>
<ul class="list-group mb-3">
<li v-for="(u, i) in usages" :key="i"
class="list-group-item d-flex justify-content-between
align-items-center gap-3">
<span class="me-auto">
<span class="d-block">{{ u.label || '—' }}</span>
<!-- The value alone says nothing about where the entry
sits. The table is a database name and stays as it is. -->
<code class="small text-muted">{{ u.table }}</code>
</span>
<span v-if="u.table === 'campus.tbl_infoscreen_content'"
class="badge bg-danger text-nowrap">
{{ $p.t('cms/wirdMitgeloescht') }}
</span>
<span v-else class="badge bg-warning text-dark text-nowrap">
{{ $p.t('cms/verhindertLoeschen') }}
</span>
</li>
</ul>
</div>
</template>
<button class="btn btn-danger" @click="deleteContent"
:disabled="hasRestrict || loadingUsage">
{{ $p.t('cms/ganzenContentLoeschen') }}
</button>
</div>
`
};
-332
View File
@@ -1,332 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsHierarchy',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['reload-content-info', 'reload-tree', 'refresh-tree-node', 'select-content'],
data() {
return {
parents: [],
childs: [],
possibleChilds: [],
selectedChild: '',
dragIndex: null,
orderBefore: null,
dropped: false,
savingOrder: false
};
},
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.loadParents();
this.loadChilds();
this.loadPossibleChilds();
},
loadParents() {
if (this.contentId == null || !this.sprache) return;
this.$api
.call(ApiCmsAdmin.getParents(this.contentId, this.sprache))
.then(result => { this.parents = result.data || []; })
.catch(this.$fhcAlert.handleSystemError);
},
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();
// Only this branch changes, so refresh this branch.
this.$emit('refresh-tree-node', this.contentId);
this.$fhcAlert.alertSuccess(this.$p.t('cms/childHinzugefuegt'));
} 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('refresh-tree-node', this.contentId);
this.$fhcAlert.alertSuccess(this.$p.t('cms/childEntfernt'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
},
// Detaches this content from one parent. That is the same row as a child
// assignment, seen from the other end, so it uses the same endpoint.
async removeParent(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.loadParents();
// The content leaves one branch and may become a root, so rebuild the tree.
this.$emit('reload-tree');
this.$fhcAlert.alertSuccess(this.$p.t('cms/childEntfernt'));
} 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('refresh-tree-node', this.contentId);
this.$fhcAlert.alertSuccess(this.$p.t('cms/sortierungGespeichert'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
},
// --- Drag and drop ---
// The dragged row follows the cursor: every row it enters swaps places with it.
// The drop then only has to write the order that already stands on screen.
onDragStart(index, event) {
this.dragIndex = index;
this.dropped = false;
this.orderBefore = this.childs.map(c => c.contentchild_id);
event.dataTransfer.effectAllowed = 'move';
// Firefox starts no drag without a payload.
event.dataTransfer.setData('text/plain', String(index));
},
onDragEnter(index) {
if (this.dragIndex === null || this.dragIndex === index) return;
const moved = this.childs.splice(this.dragIndex, 1)[0];
this.childs.splice(index, 0, moved);
this.dragIndex = index;
},
onDrop() {
this.dropped = true;
},
onDragEnd() {
if (this.dragIndex === null) return;
const before = this.orderBefore;
this.dragIndex = null;
this.orderBefore = null;
// Escape cancels the drag. Put the rows back.
if (!this.dropped) {
this.applyOrder(before);
return;
}
if (this.orderDiffers(before)) this.saveOrder(before);
},
applyOrder(contentchild_ids) {
if (!contentchild_ids) return;
const byId = new Map(this.childs.map(c => [c.contentchild_id, c]));
this.childs = contentchild_ids.map(id => byId.get(id)).filter(c => c);
},
orderDiffers(contentchild_ids) {
if (!contentchild_ids) return false;
return this.childs.some((c, i) => c.contentchild_id !== contentchild_ids[i]);
},
// One request for the whole order. A child moves over any distance, and the arrow
// endpoint swaps two neighbours, so it would need one request per position.
async saveOrder(before) {
this.savingOrder = true;
try {
await this.$api.call(ApiCmsAdmin.putChildOrder(
this.contentId,
this.childs.map(c => c.contentchild_id)
));
this.loadChilds();
this.$emit('refresh-tree-node', this.contentId);
this.$fhcAlert.alertSuccess(this.$p.t('cms/sortierungGespeichert'));
} catch (e) {
this.applyOrder(before);
this.$fhcAlert.handleSystemError(e);
} finally {
this.savingOrder = false;
}
},
onContentClick(content_id) {
this.$emit('select-content', content_id);
},
isFirst(index) {
return index === 0;
},
isLast(index) {
return index === this.childs.length - 1;
}
},
mounted() {
this.loadAll();
},
template: `
<div class="p-3" v-if="contentId != null">
<h5>{{ $p.t('cms/folgendeEintraegeSindUebergeordnet') }}</h5>
<table v-if="parents.length > 0" class="table table-sm table-striped">
<thead>
<tr>
<th>ID</th>
<th>{{ $p.t('cms/titel') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="parent in parents" :key="parent.contentchild_id">
<td>{{ parent.content_id }}</td>
<td>
<a href="#" @click.prevent="onContentClick(parent.content_id)">
{{ parent.titel || $p.t('cms/ohneTitel') }}
</a>
</td>
<td>
<button class="btn btn-sm btn-outline-danger"
@click="removeParent(parent.contentchild_id)">
{{ $p.t('cms/entfernen') }}
</button>
</td>
</tr>
</tbody>
</table>
<div v-else class="text-muted">
{{ $p.t('cms/keineUebergeordnetenEintraege') }}
</div>
<h5 class="mt-4">{{ $p.t('cms/folgendeEintraegeSindUntergeordnet') }}</h5>
<template v-if="childs.length > 0">
<div class="form-text mb-1">
<i v-if="savingOrder" class="fa-solid fa-spinner fa-spin me-1"></i>
{{ $p.t('cms/ziehenZumSortieren') }}
</div>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>{{ $p.t('cms/sortierung') }}</th>
<th>ID</th>
<th>{{ $p.t('cms/titel') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(child, idx) in childs" :key="child.contentchild_id"
draggable="true"
:style="dragIndex === idx ? 'opacity:0.4' : ''"
@dragstart="onDragStart(idx, $event)"
@dragenter.prevent="onDragEnter(idx)"
@dragover.prevent
@drop.prevent="onDrop"
@dragend="onDragEnd">
<td class="text-nowrap">
<i class="fa-solid fa-grip-vertical text-muted me-1"
style="cursor:grab"
:title="$p.t('cms/ziehenZumSortieren')"></i>
{{ idx + 1 }}
<button class="btn btn-sm btn-link p-0 ms-1"
:disabled="isFirst(idx)"
@click="moveSort(child.contentchild_id, 'up')">
<i class="fa-solid fa-arrow-up"></i>
</button>
<button class="btn btn-sm btn-link p-0 ms-1"
:disabled="isLast(idx)"
@click="moveSort(child.contentchild_id, 'down')">
<i class="fa-solid fa-arrow-down"></i>
</button>
</td>
<td>{{ child.child_content_id }}</td>
<td>
<a href="#" draggable="false"
@click.prevent="onContentClick(child.child_content_id)">
{{ child.titel || $p.t('cms/ohneTitel') }}
</a>
</td>
<td>
<button class="btn btn-sm btn-outline-danger"
@click="removeChild(child.contentchild_id)">
{{ $p.t('cms/entfernen') }}
</button>
</td>
</tr>
</tbody>
</table>
</template>
<div v-else class="text-muted">
{{ $p.t('cms/keineUntereintraege') }}
</div>
<div class="d-flex gap-2 align-items-end mt-3">
<select class="form-select" v-model="selectedChild" style="max-width: 500px;">
<option value="" disabled></option>
<option v-for="c in availableChilds" :key="c.content_id"
:value="c.content_id">
{{ childLabel(c) }}
</option>
</select>
<button class="btn btn-primary" @click="addChild" :disabled="!selectedChild">
{{ $p.t('cms/hinzufuegen') }}
</button>
</div>
</div>
`
};
-65
View File
@@ -1,65 +0,0 @@
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: `
<div class="p-3" v-if="contentId != null">
<h5>{{ $p.t('cms/versionen') }}</h5>
<div v-for="v in versions" :key="v.version"
class="mb-3 p-2 border rounded"
:class="{ 'border-primary border-2': v.version === version }">
<div class="fw-bold">
{{ $p.t('cms/version') }} {{ v.version }}
<span v-if="!v.sichtbar" class="badge bg-secondary ms-2">
{{ $p.t('cms/unsichtbar') }}
</span>
</div>
<div>{{ $p.t('cms/erstelltAmVon', [formatDate(v.insertamum), v.insertvon]) }}</div>
<div v-if="v.updatevon">
{{ $p.t('cms/letzteAenderungVonAm', [formatDate(v.updateamum), v.updatevon]) }}
</div>
</div>
</div>
`
};
@@ -1,143 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsPermissions',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['reload-content-info', 'patch-tree-node', '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 Promise.resolve();
return this.$api
.call(ApiCmsAdmin.getGruppen(this.contentId))
.then(result => {
this.gruppen = result.data || [];
this.selectedGruppe = '';
})
.catch(this.$fhcAlert.handleSystemError);
},
// A group only draws the lock icon. It never moves a node, so the tree needs no
// request here.
patchTree() {
this.$emit('patch-tree-node', {
content_id: this.contentId,
patch: { groups: this.gruppen.map(g => g.gruppe_kurzbz) }
});
},
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)
);
await this.loadGruppen();
this.patchTree();
this.$fhcAlert.alertSuccess(this.$p.t('cms/gruppeHinzugefuegt'));
} 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)
);
await this.loadGruppen();
this.patchTree();
this.$fhcAlert.alertSuccess(this.$p.t('cms/gruppeEntfernt'));
} catch (e) {
this.$fhcAlert.handleSystemError(e);
}
}
},
mounted() {
this.loadAllGruppen();
this.loadGruppen();
},
template: `
<div class="p-3" v-if="contentId != null">
<template v-if="gruppen.length > 0">
<h5>{{ $p.t('cms/folgendeGruppenDuerfenAnsehen') }}</h5>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>{{ $p.t('cms/gruppe') }}</th>
<th>{{ $p.t('cms/bezeichnung') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="g in gruppen" :key="g.gruppe_kurzbz">
<td>{{ g.gruppe_kurzbz }}</td>
<td>{{ g.bezeichnung }}</td>
<td>
<button class="btn btn-sm btn-outline-danger"
@click="removeGruppe(g.gruppe_kurzbz)">
{{ $p.t('cms/entfernen') }}
</button>
</td>
</tr>
</tbody>
</table>
</template>
<div v-else class="alert alert-warning">
{{ $p.t('cms/seiteDarfVonAllenAngezeigtWerden') }}
</div>
<div class="d-flex gap-2 align-items-end mt-3">
<select class="form-select" v-model="selectedGruppe" style="max-width: 400px;">
<option value="" disabled></option>
<option v-for="g in availableGruppen" :key="g.gruppe_kurzbz"
:value="g.gruppe_kurzbz">
{{ g.gruppe_kurzbz }} — {{ g.bezeichnung }}
</option>
</select>
<button class="btn btn-primary" @click="addGruppe" :disabled="!selectedGruppe">
{{ $p.t('cms/hinzufuegen') }}
</button>
</div>
</div>
`
};
-185
View File
@@ -1,185 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
export default {
name: 'CmsProperties',
props: {
contentId: Number,
sprache: String,
version: Number,
contentInfo: Object
},
emits: ['reload-content-info', 'refresh-tree-node', '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');
// Title, active flag and organisational unit all show in the tree.
this.$emit('refresh-tree-node', this.contentId);
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.saving = false; });
}
},
mounted() {
this.loadLookups();
this.loadData();
},
template: `
<div class="p-3" v-if="contentId != null">
<h5>{{ $p.t('cms/giltFuerAlleSprachen') }}</h5>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">{{ $p.t('cms/vorlage') }}</label>
<div class="col-sm-9">
<select class="form-select" v-model="template_kurzbz"
@change="onTemplateChange">
<option v-for="t in templates" :key="t.template_kurzbz"
:value="t.template_kurzbz">{{ t.bezeichnung }}</option>
</select>
<div v-if="templateChanged" class="text-danger mt-1 fw-bold">
{{ $p.t('cms/vorlageWarnung') }}
</div>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">{{ $p.t('cms/organisationseinheit') }}</label>
<div class="col-sm-9">
<select class="form-select" v-model="oe_kurzbz">
<option v-for="oe in organisationseinheiten" :key="oe.oe_kurzbz"
:value="oe.oe_kurzbz"
:class="{ 'text-muted': !oe.aktiv }">{{ oeLabel(oe) }}</option>
</select>
</div>
</div>
<div class="mb-3 row">
<div class="col-sm-9 offset-sm-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="aktiv"
id="prop-aktiv">
<label class="form-check-label" for="prop-aktiv">
{{ $p.t('cms/aktiv') }}
</label>
</div>
</div>
</div>
<div class="mb-3 row">
<div class="col-sm-9 offset-sm-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="menu_open"
id="prop-menu-open">
<label class="form-check-label" for="prop-menu-open">
{{ $p.t('cms/menuOpen') }}
</label>
</div>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">{{ $p.t('cms/beschreibung') }}</label>
<div class="col-sm-9">
<textarea class="form-control" rows="3" v-model="beschreibung"></textarea>
</div>
</div>
<hr>
<h5>{{ $p.t('cms/giltFuerDieseVersion') }}</h5>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">{{ $p.t('cms/titel') }}</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="titel" maxlength="256">
</div>
</div>
<div class="mb-3 row">
<div class="col-sm-9 offset-sm-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" v-model="sichtbar"
id="prop-sichtbar">
<label class="form-check-label" for="prop-sichtbar">
{{ $p.t('cms/sichtbar') }}
</label>
</div>
</div>
</div>
<hr>
<div class="d-flex flex-wrap gap-2 mb-3">
<button class="btn btn-primary" @click="save" :disabled="saving">
{{ $p.t('cms/speichern') }}
</button>
</div>
</div>
`
};
@@ -1,521 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
import TreeFilter, { emptyCriteria } from './TreeFilter.js';
import MenuSwitch from './MenuSwitch.js';
// The meta dates are SQL timestamps. Format them like the history tab does.
function formatDay(value) {
if (!value) return '';
const dt = luxon.DateTime.fromSQL(value);
return dt.isValid ? dt.toFormat('dd.MM.yyyy') : value;
}
// A day string of the meta value. The inputs deliver YYYY-MM-DD, so compare as text.
function inRange(value, from, to) {
if (!from && !to) return true;
if (!value) return false;
const day = value.substring(0, 10);
if (from && day < from) return false;
if (to && day > to) return false;
return true;
}
// The meta fields that hold a number. The rest hold an SQL timestamp.
const NUMERIC_FIELDS = ['childcount', 'contentlength'];
// Sort key of one node. A content without meta sorts last in a descending list.
function sortValue(meta, contentId, field) {
const numeric = NUMERIC_FIELDS.indexOf(field) !== -1;
const m = meta[contentId];
if (!m) return numeric ? -1 : '';
if (numeric) return m[field];
return m[field] || '';
}
const TreeNode = {
name: 'CmsTreeNode',
inject: ['treeSelect', 'treeToggle'],
props: {
node: Object,
activeContentId: Number,
expandedKeys: Object,
flat: Boolean,
meta: Object,
metric: String,
// Ancestors of this node, from the top down. Only the nodes of the flat result
// list get one; a child inside the subtree sits under its parent already.
ancestors: Array,
// A node that is context rather than a hit. It and its children draw back.
muted: Boolean
},
computed: {
// Shows the value the active filter or sort works on, so the order is readable.
badge() {
if (!this.metric || !this.meta) return null;
const m = this.meta[this.node.content_id];
if (!m) return null;
if (NUMERIC_FIELDS.indexOf(this.metric) !== -1) return String(m[this.metric]);
return formatDay(m[this.metric]) || null;
},
badgeEmpty() {
return NUMERIC_FIELDS.indexOf(this.metric) !== -1 && this.badge === '0';
},
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;
},
// The hit keeps its place; the path above it steps in one level at a time.
indent() {
return this.ancestors ? this.ancestors.length : 0;
},
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: `
<li class="py-1" :class="{ 'cms-tree-context': muted }">
<div v-for="(a, i) in ancestors" :key="a.content_id"
class="cms-tree-ancestor"
:style="{ marginLeft: (i * 1.1) + 'em' }">
<!-- treeSelect reads entitled and content_id, so an ancestor row is a node
as far as it is concerned. An ancestor of a unit the editor is not
entitled for stays plain text, the same as in the tree itself. -->
<a v-if="a.entitled" href="#"
@click.prevent="treeSelect(a)"
>{{ a.titel || a.content_id }} ({{ a.content_id }})</a>
<span v-else>{{ a.titel || a.content_id }} ({{ a.content_id }})</span>
</div>
<div :style="indent ? { marginLeft: (indent * 1.1) + 'em' } : null">
<span v-if="!flat && hasChildren"
style="cursor:pointer;user-select:none"
@click="treeToggle(node)">
<i class="fa-solid fa-fw"
:class="isExpanded ? 'fa-caret-down' : 'fa-caret-right'"></i>
</span>
<span v-else-if="!flat" style="display:inline-block;width:1.25em"></span>
<a v-if="node.entitled"
href="#"
class="text-decoration-none"
:style="labelStyle"
@click.prevent="treeSelect(node)">{{ node.titel }} ({{ node.content_id }})</a>
<span v-else :style="!node.aktiv ? 'opacity:0.5' : ''">{{ node.titel }} ({{ node.content_id }})</span>
<i v-if="node.groups && node.groups.length"
class="fa-solid fa-lock fa-sm ms-1 cms-tree-lock"
:title="node.groups.join(', ')"></i>
<span v-if="badge !== null"
class="badge ms-1"
:class="badgeEmpty ? 'bg-light text-muted' : 'bg-secondary'">{{ badge }}</span>
<ul v-if="!flat && isExpanded && hasChildren" class="list-unstyled ms-3">
<cms-tree-node
v-for="child in node.children"
:key="child.content_id"
:node="child"
:active-content-id="activeContentId"
:expanded-keys="expandedKeys"
:flat="flat"
:meta="meta"
:metric="metric"
:muted="muted || indent > 0"
></cms-tree-node>
</ul>
</div>
</li>
`
};
export default {
name: 'CmsTree',
components: {
'cms-tree-node': TreeNode,
'tree-filter': TreeFilter,
'menu-switch': MenuSwitch
},
props: {
activeContentId: Number
},
emits: ['select-content'],
data() {
const stored = localStorage.getItem('cms/menu');
return {
menu: (stored === 'content' || stored === 'news') ? stored : 'content',
filter: '',
nodes: [],
loading: false,
expandedKeys: {},
criteria: emptyCriteria(),
meta: null,
metaLoading: false,
// A flat list drops the context a generic title needs. The toggle puts the
// way down to each hit back, and the choice survives a page switch.
showHierarchy: localStorage.getItem('cms/treeHierarchie') === '1',
ancestors: {},
ancestorsLoading: false
};
},
computed: {
// The news menu is a flat list without the meta fields.
advanced() {
return this.menu === 'content';
},
// Every field but the template comes from the meta request.
needsMeta() {
const c = this.criteria;
return c.mine
|| !!c.updatedFrom || !!c.updatedTo
|| !!c.createdFrom || !!c.createdTo
|| c.minChildren !== '' || c.maxChildren !== ''
|| c.minLength !== '' || c.maxLength !== '' || c.emptyOnly
|| c.sort === 'updateamum' || c.sort === 'insertamum'
|| c.sort === 'childcount' || c.sort === 'contentlength';
},
narrowing() {
const c = this.criteria;
return !!c.template_kurzbz || c.mine
|| !!c.updatedFrom || !!c.updatedTo
|| !!c.createdFrom || !!c.createdTo
|| c.minChildren !== '' || c.maxChildren !== ''
|| c.minLength !== '' || c.maxLength !== '' || c.emptyOnly;
},
active() {
return this.advanced && (this.narrowing || this.criteria.sort !== '');
},
// A hierarchical tree already shows where a node sits. Only a flat list, from a
// search or from a filter, needs the path spelled out.
isFlat() {
return this.active || this.menu === 'news' || !!this.filter;
},
showPaths() {
return this.showHierarchy && this.isFlat;
},
// The value the badge shows. The filter drives it, the sort order falls back.
metric() {
const c = this.criteria;
if (!this.active) return null;
if (c.emptyOnly || c.minLength !== '' || c.maxLength !== '') return 'contentlength';
if (c.minChildren !== '' || c.maxChildren !== '') return 'childcount';
if (c.updatedFrom || c.updatedTo) return 'updateamum';
if (c.createdFrom || c.createdTo) return 'insertamum';
if (c.sort !== '' && c.sort !== 'titel') return c.sort;
return null;
},
// Flat on purpose: a match sits at any depth, so per-level sorting would hide it.
visibleNodes() {
if (!this.active) return this.nodes;
const flatList = [];
const walk = (nodes) => {
for (const node of nodes) {
flatList.push(node);
if (node.children && node.children.length) walk(node.children);
}
};
walk(this.nodes);
return this.sortNodes(flatList.filter(node => this.matches(node)));
}
},
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: {
// Keeps the expanded branches. A reload must not fold the tree back up.
loadTree() {
this.loading = true;
return this.$api
.call(ApiCmsAdmin.getTree(this.menu, this.filter))
.then(result => {
this.nodes = result.data;
if (this.activeContentId != null) {
this.expandPathTo(this.activeContentId);
}
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.loading = false; });
},
// --- Public: the parent calls these instead of rebuilding the component ---
reload() {
// A change moves a date or a child count. Drop the cached meta.
this.meta = null;
if (this.needsMeta) this.loadMeta();
return this.loadTree();
},
// Replaces one branch. Use it after a change to the children, the title, the
// active flag or the organisational unit of that content.
refreshNode(contentId) {
// The news menu is a flat list of its own. Only a full reload fits it.
if (this.menu === 'news' || contentId == null) return this.loadTree();
const slot = this.findSlot(this.nodes, contentId);
// Not in the tree, so the change may add a new root. Rebuild.
if (slot === null) return this.loadTree();
return this.$api
.call(ApiCmsAdmin.getSubtree(contentId))
.then(result => {
if (result.data) {
slot.list.splice(slot.index, 1, result.data);
} else {
slot.list.splice(slot.index, 1);
}
if (this.activeContentId != null) {
this.expandPathTo(this.activeContentId);
}
})
.catch(this.$fhcAlert.handleSystemError);
},
// Writes known fields straight into one node. Groups do not change the structure,
// so they need no request.
patchNode(contentId, patch) {
const slot = this.findSlot(this.nodes, contentId);
if (slot === null) return;
Object.assign(slot.node, patch);
},
findSlot(nodes, contentId) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].content_id === contentId) {
return { list: nodes, index: i, node: nodes[i] };
}
if (nodes[i].children && nodes[i].children.length) {
const found = this.findSlot(nodes[i].children, contentId);
if (found !== null) return found;
}
}
return null;
},
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);
}
},
matches(node) {
const c = this.criteria;
if (c.template_kurzbz && node.template_kurzbz !== c.template_kurzbz) return false;
if (!this.needsMeta) return true;
// The meta arrives after the tree. Hide nothing until it is there.
if (this.meta === null) return true;
const m = this.meta[node.content_id];
if (!m) return false;
if (c.mine && !m.mine) return false;
if (!inRange(m.updateamum, c.updatedFrom, c.updatedTo)) return false;
if (!inRange(m.insertamum, c.createdFrom, c.createdTo)) return false;
if (c.minChildren !== '' && m.childcount < Number(c.minChildren)) return false;
if (c.maxChildren !== '' && m.childcount > Number(c.maxChildren)) return false;
if (c.emptyOnly && m.contentlength !== 0) return false;
if (c.minLength !== '' && m.contentlength < Number(c.minLength)) return false;
if (c.maxLength !== '' && m.contentlength > Number(c.maxLength)) return false;
return true;
},
// Descending for the numbers and the dates: the largest and the newest come first.
sortNodes(list) {
const sort = this.criteria.sort;
if (!sort) return list;
if (sort === 'titel') {
return list.slice().sort(
(a, b) => (a.titel || '').localeCompare(b.titel || '')
);
}
const meta = this.meta || {};
return list.slice().sort((a, b) => {
const va = sortValue(meta, a.content_id, sort);
const vb = sortValue(meta, b.content_id, sort);
if (va === vb) return 0;
return va < vb ? 1 : -1;
});
},
// One request for the whole tree. Cache it, the filter changes often.
loadMeta() {
if (this.meta !== null || this.metaLoading) return;
this.metaLoading = true;
this.$api
.call(ApiCmsAdmin.getTreeMeta())
.then(result => { this.meta = result.data || {}; })
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.metaLoading = false; });
},
toggleHierarchy() {
this.showHierarchy = !this.showHierarchy;
localStorage.setItem('cms/treeHierarchie', this.showHierarchy ? '1' : '0');
if (!this.showHierarchy) return;
// The point of the toggle is the context around a hit, and the subtree is half
// of it. A collapsed node would show the way down to the hit and nothing under
// it. Opening the hits leaves every caret free to collapse again.
for (const node of this.visibleNodes)
this.expandedKeys[node.content_id] = true;
this.loadAncestors();
},
// One request for the whole list. Asking per node would be a request per hit.
loadAncestors() {
if (!this.showPaths) return;
const ids = this.visibleNodes.map(node => node.content_id);
if (!ids.length) {
this.ancestors = {};
return;
}
this.ancestorsLoading = true;
this.$api
.call(ApiCmsAdmin.getAncestors(ids))
.then(result => { this.ancestors = result.data || {}; })
.catch(this.$fhcAlert.handleSystemError)
.finally(() => { this.ancestorsLoading = false; });
},
pathFor(contentId) {
return this.showPaths ? (this.ancestors[contentId] || []) : null;
},
onCriteria(criteria) {
this.criteria = criteria;
if (this.needsMeta) this.loadMeta();
this.loadAncestors();
},
onFilter(text) {
this.filter = text;
this.loadTree().then(() => { this.loadAncestors(); });
},
createEntry(parentId) {
this.$api
.call(ApiCmsAdmin.postContent(parentId))
.then(result => {
const newId = result.data.content_id;
this.$fhcAlert.alertSuccess(this.$p.t('cms/eintragAngelegt'));
// A new root is not under any branch, so only a reload shows it.
const refreshed = parentId == null
? this.loadTree()
: this.refreshNode(parentId);
// Select after the node exists, or expandPathTo finds nothing.
return refreshed.then(() => {
this.$emit('select-content', newId);
});
})
.catch(this.$fhcAlert.handleSystemError);
}
},
mounted() {
this.loadTree();
},
template: `
<div class="p-2">
<menu-switch v-model="menu"></menu-switch>
<tree-filter class="mt-2"
:advanced="advanced"
@filter="onFilter"
@criteria="onCriteria"></tree-filter>
<div class="mt-2 mb-2">
<button class="btn btn-sm btn-outline-secondary me-1"
@click="createEntry(null)">
{{ $p.t('cms/neuenEintragHinzufuegen') }}
</button>
<button class="btn btn-sm btn-outline-secondary"
:disabled="activeContentId == null"
@click="createEntry(activeContentId)">
{{ $p.t('cms/neuenChildEintragHinzufuegen') }}
</button>
</div>
<div v-if="isFlat && !loading" class="d-flex align-items-center gap-2 mb-2">
<div v-if="active" class="form-text m-0">
<i v-if="metaLoading" class="fa-solid fa-spinner fa-spin me-1"></i>
{{ $p.t('cms/nTreffer', [visibleNodes.length]) }}
</div>
<button type="button"
class="btn btn-sm ms-auto"
:class="showHierarchy ? 'btn-secondary' : 'btn-outline-secondary'"
@click="toggleHierarchy">
<i class="fa-solid fa-fw"
:class="ancestorsLoading ? 'fa-spinner fa-spin' : 'fa-sitemap'"></i>
{{ $p.t('cms/hierarchieAnzeigen') }}
</button>
</div>
<div v-if="loading" class="text-center text-muted py-3">
<i class="fa-solid fa-spinner fa-spin"></i>
</div>
<div v-else-if="visibleNodes?.length === 0" class="text-muted py-2">
{{ (filter || active) ? $p.t('cms/keineTreffer') : $p.t('cms/keineEintraege') }}
</div>
<ul v-else class="list-unstyled mb-0">
<cms-tree-node
v-for="node in visibleNodes"
:key="node.content_id"
:node="node"
:active-content-id="activeContentId"
:expanded-keys="expandedKeys"
:flat="isFlat && !showPaths"
:meta="meta"
:metric="metric"
:ancestors="pathFor(node.content_id)"
></cms-tree-node>
</ul>
</div>
`
};
@@ -1,35 +0,0 @@
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: `
<div class="btn-group btn-group-sm w-100" role="group">
<button type="button" class="btn"
:class="modelValue === 'content' ? 'btn-primary' : 'btn-outline-secondary'"
@click="select('content')">
{{ $p.t('cms/menuContent') }}
</button>
<button type="button" class="btn"
:class="modelValue === 'news' ? 'btn-primary' : 'btn-outline-secondary'"
@click="select('news')">
{{ $p.t('cms/menuNews') }}
</button>
</div>
`
};
-209
View File
@@ -1,209 +0,0 @@
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
// Empty means "do not narrow by this field". Keep the inputs as strings, the tree parses.
function emptyCriteria() {
return {
template_kurzbz: '',
mine: false,
updatedFrom: '',
updatedTo: '',
createdFrom: '',
createdTo: '',
minChildren: '',
maxChildren: '',
minLength: '',
maxLength: '',
emptyOnly: false,
sort: ''
};
}
export { emptyCriteria };
export default {
name: 'CmsTreeFilter',
props: {
// The news menu is a flat list of its own. It carries none of these fields.
advanced: { type: Boolean, default: true }
},
emits: ['filter', 'criteria'],
data() {
return {
text: '',
open: false,
templates: [],
criteria: emptyCriteria()
};
},
computed: {
// Counts the narrowing fields only. The sort order hides no entry.
activeCount() {
const c = this.criteria;
let count = 0;
if (c.template_kurzbz) count++;
if (c.mine) count++;
if (c.updatedFrom || c.updatedTo) count++;
if (c.createdFrom || c.createdTo) count++;
if (c.minChildren !== '' || c.maxChildren !== '') count++;
if (c.minLength !== '' || c.maxLength !== '') count++;
if (c.emptyOnly) count++;
return count;
}
},
methods: {
submit() {
this.$emit('filter', this.text);
},
toggle() {
this.open = !this.open;
if (this.open && !this.templates.length) this.loadTemplates();
},
loadTemplates() {
this.$api
.call(ApiCmsAdmin.getTemplates())
.then(result => { this.templates = result.data || []; })
.catch(this.$fhcAlert.handleSystemError);
},
reset() {
this.text = '';
// The watcher emits the cleared criteria.
this.criteria = emptyCriteria();
this.$emit('filter', '');
}
},
watch: {
// v-model writes before the watcher runs, so the emitted object is complete.
criteria: {
handler(criteria) { this.$emit('criteria', { ...criteria }); },
deep: true
}
},
created() {
this.sortOptions = [
{ value: '', phrase: 'cms/sortHierarchie' },
{ value: 'titel', phrase: 'cms/titel' },
{ value: 'updateamum', phrase: 'cms/sortGeaendert' },
{ value: 'insertamum', phrase: 'cms/sortErstellt' },
{ value: 'childcount', phrase: 'cms/sortChilds' },
{ value: 'contentlength', phrase: 'cms/sortInhaltslaenge' }
];
},
template: `
<div>
<div class="input-group input-group-sm">
<input type="text"
class="form-control"
:placeholder="$p.t('cms/filterPlatzhalter')"
v-model="text"
@keydown.enter="submit">
<button class="btn btn-outline-secondary" type="button" @click="submit">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
<button v-if="advanced"
type="button"
class="btn btn-sm btn-link px-0 text-decoration-none"
@click="toggle">
<i class="fa-solid fa-fw" :class="open ? 'fa-caret-down' : 'fa-caret-right'"></i>
{{ $p.t('cms/filterErweitert') }}
<span v-if="activeCount" class="badge bg-primary ms-1">{{ activeCount }}</span>
</button>
<div v-if="advanced && open" class="border rounded p-2 mb-2">
<div class="mb-2">
<label class="form-label mb-1">{{ $p.t('cms/vorlage') }}</label>
<select class="form-select form-select-sm"
v-model="criteria.template_kurzbz">
<option value="">{{ $p.t('cms/alleVorlagen') }}</option>
<option v-for="t in templates"
:key="t.template_kurzbz"
:value="t.template_kurzbz">{{ t.bezeichnung }}</option>
</select>
</div>
<div class="form-check mb-2">
<input type="checkbox"
class="form-check-input"
id="cms-filter-mine"
v-model="criteria.mine">
<label class="form-check-label" for="cms-filter-mine">
{{ $p.t('cms/nurEigeneBearbeitungen') }}
</label>
</div>
<div class="form-check mb-2">
<input type="checkbox"
class="form-check-input"
id="cms-filter-empty"
v-model="criteria.emptyOnly">
<label class="form-check-label" for="cms-filter-empty">
{{ $p.t('cms/nurLeereInhalte') }}
</label>
</div>
<div class="mb-2">
<label class="form-label mb-1">{{ $p.t('cms/sortGeaendert') }}</label>
<div class="input-group input-group-sm">
<input type="date" class="form-control"
:title="$p.t('cms/vonDatum')"
v-model="criteria.updatedFrom">
<input type="date" class="form-control"
:title="$p.t('cms/bisDatum')"
v-model="criteria.updatedTo">
</div>
</div>
<div class="mb-2">
<label class="form-label mb-1">{{ $p.t('cms/sortErstellt') }}</label>
<div class="input-group input-group-sm">
<input type="date" class="form-control"
:title="$p.t('cms/vonDatum')"
v-model="criteria.createdFrom">
<input type="date" class="form-control"
:title="$p.t('cms/bisDatum')"
v-model="criteria.createdTo">
</div>
</div>
<div class="mb-2">
<label class="form-label mb-1">{{ $p.t('cms/sortChilds') }}</label>
<div class="input-group input-group-sm">
<input type="number" min="0" class="form-control"
:placeholder="$p.t('cms/mindestens')"
v-model="criteria.minChildren">
<input type="number" min="0" class="form-control"
:placeholder="$p.t('cms/hoechstens')"
v-model="criteria.maxChildren">
</div>
</div>
<div class="mb-2">
<label class="form-label mb-1">{{ $p.t('cms/sortInhaltslaenge') }}</label>
<div class="input-group input-group-sm">
<input type="number" min="0" class="form-control"
:placeholder="$p.t('cms/mindestens')"
v-model="criteria.minLength">
<input type="number" min="0" class="form-control"
:placeholder="$p.t('cms/hoechstens')"
v-model="criteria.maxLength">
</div>
</div>
<div class="mb-2">
<label class="form-label mb-1">{{ $p.t('cms/sortierung') }}</label>
<select class="form-select form-select-sm"
v-model="criteria.sort">
<option v-for="o in sortOptions" :key="o.value" :value="o.value">
{{ $p.t(o.phrase) }}
</option>
</select>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" @click="reset">
{{ $p.t('cms/filterZuruecksetzen') }}
</button>
</div>
</div>
`
};
@@ -1,84 +0,0 @@
import ApiCms from '../../api/factory/cms.js';
/**
* Content component: lists the child pages of a content as a menu.
*
* It replaces a hand written box of content.php links. Those links carry a fixed
* content_id and break when a page moves. This list follows tbl_contentchild, which the
* Kindelemente tab of the CMS admin already maintains.
*
* Without the content-id property the menu shows the children of the page it sits on.
* With it, the menu shows the children of another page. The second form exists because
* the same menu appears on several pages today.
*/
export default {
name: 'ContentchildMenu',
inject: {
contentcomponentContentId: { default: null }
},
props: {
contentId: {
type: [Number, String],
default: null
}
},
data() {
return {
kinder: [],
loading: true,
failed: false
};
},
computed: {
sprache() {
return this.$p.user_language.value;
},
// The property wins. Without it the menu takes the page it sits on.
// Vue unwraps an injected ref on the instance, so no .value here.
quelle() {
return this.contentId || this.contentcomponentContentId;
}
},
methods: {
load() {
if (!this.quelle)
{
this.loading = false;
return;
}
this.loading = true;
this.failed = false;
this.$api
.call(ApiCms.getContentChilds(this.quelle, this.sprache))
.then(res => { this.kinder = res.data || []; })
.catch(() => { this.failed = true; })
.finally(() => { this.loading = false; });
}
},
watch: {
quelle() { this.load(); },
// The titles come per language, so a language switch needs a new request.
sprache() { this.load(); }
},
created() {
this.load();
},
template: /*html*/ `
<div class="fhc-contentcomponent-contentchildmenu">
<div v-if="loading" class="text-muted">...</div>
<div v-else-if="failed" class="alert alert-warning py-2">
{{ $p.t('cms/ccLadefehlerMenue') }}
</div>
<!-- An empty menu renders nothing. A reader has no use for the note that a
page has no children, and the group rule may have removed every entry. -->
<ul v-else-if="kinder.length" class="list-unstyled mb-0">
<li v-for="kind in kinder" :key="kind.content_id" class="mb-1">
<router-link :to="{ name: 'Content', params: { content_id: kind.content_id } }">
{{ kind.titel }}
</router-link>
</li>
</ul>
</div>
`
};
@@ -1,78 +0,0 @@
import { components } from './components.js';
import { scanContentcomponents } from './scan.js';
/**
* Renders CMS content HTML and mounts the content components it declares.
*
* Use it in place of a plain `<div v-html="content">`. It owns the root element, the
* change watch and the Teleports, so a host site needs no ref and no lifecycle code.
*
* Teleport, not a separate createApp: the components stay in this app's component tree.
* They therefore share the one $api instance, the one $p instance, the router and the
* provided keys. A createApp per marker would build a second of each.
*/
export default {
name: 'ContentcomponentHost',
props: {
content: {
type: String,
default: ''
},
// The page this content belongs to. contentchild-menu needs it when the editor
// gave no explicit id.
contentId: {
type: [Number, String],
default: null
}
},
provide() {
// Vue.computed keeps the value live, the same way Cis.js provides its keys.
return {
contentcomponentContentId: Vue.computed(() => this.contentId)
};
},
data() {
return {
markers: []
};
},
methods: {
rescan() {
this.markers = scanContentcomponents(this.$refs.root);
},
resolve(name) {
return components[name];
}
},
watch: {
// Vue replaces the whole v-html subtree when the string changes. A marker that
// still points at a removed node makes Teleport write into a detached element,
// and the component vanishes without an error. Drop the markers first, let the
// new HTML render, then scan again.
content() {
this.markers = [];
this.$nextTick(this.rescan);
}
},
// A failing content component must not take down the content around it.
errorCaptured(err, instance, info) {
console.error('Contentcomponent failed:', info, err);
return false;
},
mounted() {
// The components below carry their texts in category cms. $p.t would fetch it by
// itself on the first miss and fill in when it arrives, which shows an empty line
// for that moment. One request here from the single gateway spares that.
this.$p.loadCategory('cms');
this.rescan();
},
beforeUnmount() {
this.markers = [];
},
template: /*html*/ `
<div ref="root" v-html="content"></div>
<Teleport v-for="marker in markers" :key="marker.key" :to="marker.el">
<component :is="resolve(marker.name)" v-bind="marker.props"></component>
</Teleport>
`
};
@@ -1,56 +0,0 @@
import ApiCms from '../../api/factory/cms.js';
import DokumentListe from './DokumentListe.js';
/**
* Content component: named DMS documents, in the order the editor gave.
*/
export default {
name: 'DmsDokumente',
components: {
DokumentListe
},
props: {
dmsIds: {
type: String,
required: true
}
},
data() {
return {
dokumente: [],
loading: true,
failed: false
};
},
methods: {
load() {
this.loading = true;
this.failed = false;
this.$api
.call(ApiCms.getDmsDokumente(this.dmsIds))
.then(res => { this.dokumente = res.data || []; })
.catch(() => { this.failed = true; })
.finally(() => { this.loading = false; });
}
},
watch: {
dmsIds() { this.load(); }
},
created() {
this.load();
},
template: /*html*/ `
<div class="fhc-contentcomponent-dmsdokumente">
<div v-if="loading" class="text-muted">...</div>
<div v-else-if="failed" class="alert alert-warning py-2">
{{ $p.t('cms/ccLadefehlerDokumente') }}
</div>
<!-- An empty list renders nothing. Every named document may have dropped out
through the category rule, and that is not the reader's business. -->
<dokument-liste
v-else-if="dokumente.length"
:dokumente="dokumente"
></dokument-liste>
</div>
`
};
@@ -1,73 +0,0 @@
import ApiCms from '../../api/factory/cms.js';
import DokumentListe from './DokumentListe.js';
/**
* Content component: the published documents of one DMS category.
*
* The low maintenance variant. It lists every document of the category whose newest
* version carries cis_suche, so a replaced or added document appears without an edit in
* the CMS. Use dms-dokumente when the editor must pick single files.
*/
export default {
name: 'DmsListe',
components: {
DokumentListe
},
props: {
kategorieKurzbz: {
type: String,
required: true
},
// 0 lists the category itself, which is what a marker without the attribute
// means. Higher takes that many levels of sub categories with it.
tiefe: {
type: Number,
default: 0
}
},
data() {
return {
dokumente: [],
zugriff: true,
loading: true,
failed: false
};
},
methods: {
load() {
this.loading = true;
this.failed = false;
this.$api
.call(ApiCms.getDmsKategorie(this.kategorieKurzbz, this.tiefe))
.then(res => {
this.zugriff = res.data.zugriff;
this.dokumente = res.data.dokumente || [];
})
.catch(() => { this.failed = true; })
.finally(() => { this.loading = false; });
}
},
watch: {
kategorieKurzbz() { this.load(); },
tiefe() { this.load(); }
},
created() {
this.load();
},
template: /*html*/ `
<div class="fhc-contentcomponent-dmsliste">
<div v-if="loading" class="text-muted">...</div>
<div v-else-if="failed" class="alert alert-warning py-2">
{{ $p.t('cms/ccLadefehlerDokumente') }}
</div>
<!-- A reader without the entitlement sees nothing at all, not a hint that
something is hidden here. -->
<template v-else-if="zugriff">
<div v-if="!dokumente.length" class="text-muted">
{{ $p.t('cms/ccKeineDokumente') }}
</div>
<dokument-liste v-else :dokumente="dokumente"></dokument-liste>
</template>
</div>
`
};
@@ -1,59 +0,0 @@
// Mimetype to Font Awesome 6 icon. The first match wins.
const ICONS = [
[/pdf/, 'fa-file-pdf'],
[/word|opendocument\.text/, 'fa-file-word'],
[/excel|sheet|csv/, 'fa-file-excel'],
[/powerpoint|presentation/, 'fa-file-powerpoint'],
[/^image\//, 'fa-file-image'],
[/zip|compressed|tar|rar|7z/, 'fa-file-zipper'],
[/^video\//, 'fa-file-video'],
[/^audio\//, 'fa-file-audio'],
[/^text\//, 'fa-file-lines']
];
/**
* Renders a list of DMS documents as download links.
*
* It fetches nothing. dms-liste and dms-dokumente both fill it, so a document reads the
* same whether the editor named a category or single ids.
*/
export default {
name: 'DokumentListe',
props: {
dokumente: {
type: Array,
default: () => []
}
},
methods: {
icon(mimetype) {
const typ = (mimetype || '').toLowerCase();
const treffer = ICONS.find(([muster]) => muster.test(typ));
return treffer ? treffer[1] : 'fa-file';
},
href(dokument) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cms/dms.php?id=' + dokument.dms_id;
},
datum(wert) {
if (!wert) return '';
const teile = String(wert).slice(0, 10).split('-');
return teile.length === 3 ? teile[2] + '.' + teile[1] + '.' + teile[0] : '';
}
},
template: /*html*/ `
<ul class="list-unstyled mb-0">
<li v-for="dokument in dokumente" :key="dokument.dms_id" class="mb-2">
<a :href="href(dokument)" target="_blank" rel="noopener">
<i class="fa-regular" :class="icon(dokument.mimetype)"></i>
{{ dokument.name }}
</a>
<span v-if="dokument.geaendert" class="text-muted small ms-2">
{{ datum(dokument.geaendert) }}
</span>
<div v-if="dokument.beschreibung" class="text-muted small">
{{ dokument.beschreibung }}
</div>
</li>
</ul>
`
};
@@ -1,85 +0,0 @@
import ApiCms from '../../api/factory/cms.js';
import PersonBlock from './PersonBlock.js';
/**
* Content component: every person assigned to an organisation unit.
*
* The low maintenance variant. A new colleague appears without an edit in the CMS, and a
* leaving one disappears. The price is that the editor cannot steer who is shown. Use
* person-block when that control matters.
*
* The function label per person comes from tbl_benutzerfunktion.bezeichnung,
* and falls back to tbl_funktion.beschreibung.
*/
export default {
name: 'OePersonen',
components: {
PersonBlock
},
props: {
oeKurzbz: {
type: String,
required: true
},
foto: {
type: Boolean,
default: false
},
// 0 lists the unit itself, which is what a marker without the attribute means.
// Higher takes that many levels of sub units with it.
tiefe: {
type: Number,
default: 0
}
},
data() {
return {
personen: [],
loading: true,
failed: false
};
},
methods: {
load() {
this.loading = true;
this.failed = false;
this.$api
.call(ApiCms.getOePersonen(this.oeKurzbz, this.foto, this.tiefe))
.then(res => { this.personen = res.data || []; })
.catch(() => { this.failed = true; })
.finally(() => { this.loading = false; });
}
},
watch: {
oeKurzbz() { this.load(); },
foto() { this.load(); },
tiefe() { this.load(); }
},
created() {
this.load();
},
template: /*html*/ `
<div class="fhc-contentcomponent-oepersonen">
<div v-if="loading" class="text-muted">...</div>
<div v-else-if="failed" class="alert alert-warning py-2">
{{ $p.t('cms/ccLadefehlerPersonen') }}
</div>
<div v-else-if="!personen.length" class="text-muted">
{{ $p.t('cms/ccKeinePersonen') }}
</div>
<template v-else>
<person-block
v-for="person in personen"
:key="person.uid"
:uid="person.uid"
:name="person.name"
:funktion="person.funktion"
:telefon="person.telefon"
:email="person.email"
:ort="person.ort"
:foto="person.foto"
></person-block>
</template>
</div>
`
};

Some files were not shown because too many files have changed in this diff Show More