aboutsummaryrefslogtreecommitdiff
path: root/lib/calendar
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:07:19 +0200
committerJan-Hendrik Willms <tleilax+github@gmail.com>2021-07-22 16:19:12 +0200
commita3da1483a9e689846179159355badfec8073dbec (patch)
tree770dcca6bdf5f6f2a11b0e7fcbbeda6919a3fc52 /lib/calendar
current code from svn, revision 62608
Diffstat (limited to 'lib/calendar')
-rw-r--r--lib/calendar/CalendarColumn.class.php364
-rw-r--r--lib/calendar/CalendarExport.class.php87
-rw-r--r--lib/calendar/CalendarExportException.class.php18
-rw-r--r--lib/calendar/CalendarExportFile.class.php122
-rw-r--r--lib/calendar/CalendarImport.class.php91
-rw-r--r--lib/calendar/CalendarImportFile.class.php141
-rw-r--r--lib/calendar/CalendarParser.class.php132
-rw-r--r--lib/calendar/CalendarParserICalendar.class.php632
-rw-r--r--lib/calendar/CalendarView.class.php340
-rw-r--r--lib/calendar/CalendarWeekView.class.php123
-rw-r--r--lib/calendar/CalendarWriter.class.php53
-rw-r--r--lib/calendar/CalendarWriterICalendar.class.php628
-rw-r--r--lib/calendar/EventData.class.php92
-rw-r--r--lib/calendar/EventSource.interface.php64
-rw-r--r--lib/calendar/lib/CalendarError.class.php56
-rw-r--r--lib/calendar/lib/ErrorHandler.class.php121
16 files changed, 3064 insertions, 0 deletions
diff --git a/lib/calendar/CalendarColumn.class.php b/lib/calendar/CalendarColumn.class.php
new file mode 100644
index 0000000..2a0821f
--- /dev/null
+++ b/lib/calendar/CalendarColumn.class.php
@@ -0,0 +1,364 @@
+<?php
+# Lifter010: TODO
+/**
+ * CalendarColumn.class.php - a column for a CalendarView
+ *
+ * This class represents an entry-column like "monday" in the calendar
+ *
+ * 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.
+ *
+ * @author Rasmus Fuhse <fuhse@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+
+class CalendarColumn {
+ protected static $number = 0;
+ protected $title = "";
+ protected $id = "";
+ public $entries = [];
+ protected $url = "";
+ protected $grouped = false;
+
+ /**
+ * creates instance of type CalendarColumn
+ *
+ * @param string $id necessary if you want JavaScript enabled for this calendar
+ * @return CalendarColumn
+ */
+ static public function create($id = null) {
+ $column = new CalendarColumn($id);
+ return $column;
+ }
+
+ /**
+ * constructor
+ *
+ * @param string $id necessary if you want JavaScript enabled for this column
+ */
+ public function __construct($id = null) {
+ $id !== null || $id = md5(uniqid("CalendarColumn_".self::$number++));
+ $this->setId($id);
+ }
+
+ /**
+ * returns the id of the column
+ *
+ * @return string
+ */
+ public function getId() {
+ return $this->id;
+ }
+
+ /**
+ * sets the id for this column, which is only necessary if you want
+ * Javascript to be enabled for this calendar
+ *
+ * @param string $id new id for this column
+ * @return CalendarColumn
+ */
+ public function setId($id) {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * sets a title like "monday" for this column, which will be displayed in the calendar
+ *
+ * @param string $new_title new title
+ * @return CalendarColumn
+ */
+ public function setTitle($new_title) {
+ $this->title = $new_title;
+ return $this;
+ }
+
+ /**
+ * returns the title of this column like "monday"
+ *
+ * @return string title of column
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * sets the url to be directed to when clicking on the title of the column.
+ * Usually this is a single-day-view of the calendar.
+ *
+ * @param string $new_url an url
+ * @return CalendarColumn
+ */
+ public function setURL($new_url) {
+ $this->url = $new_url;
+ return $this;
+ }
+
+ /**
+ * returns the URL of the column (see setURL)
+ *
+ * @return string an url
+ */
+ public function getURL() {
+ return $this->url;
+ }
+
+ /**
+ * adds a new entry in the column. The entry needs to be an associative array
+ * with parameters as follows:
+ *
+ * @param array $entry_array associative array for an entry in the column like
+ * array (
+ * 'color' => the color in hex (css-like, without the #)
+ * 'start' => the (start hour * 100) + (start minute)
+ * 'end' => the (end hour * 100) + (end minute)
+ * 'title' => the entry`s title
+ * 'content' => whatever shall be the content of the entry as a string
+ * )
+ */
+ public function addEntry($entry_array) {
+ if (!isset($entry_array['start']) || !isset($entry_array['end'])
+ || !isset($entry_array['title']) ) {
+ throw new InvalidArgumentException('The entry '. print_r($entry_array, true) .' does not follow the specifications!');
+ } else {
+ $this->entries[] = $entry_array;
+ }
+ return $this;
+ }
+
+ /**
+ * adds many entries to the column. For the syntax of an entry see addEntry()
+ *
+ * @param array $entries_array
+ * @return CalendarColumn
+ */
+ public function addEntries($entries_array = []) {
+ foreach ($entries_array as $entry_array) {
+ $this->addEntry($entry_array);
+ }
+ return $this;
+ }
+
+ /**
+ * returns all entries of this column
+ *
+ * @return array of arrays like
+ * array (
+ * 'color' => the color in hex (css-like, without the #)
+ * 'start' => the (start hour * 100) + (start minute)
+ * 'end' => the (end hour * 100) + (end minute)
+ * 'title' => the entry`s title
+ * 'content' => whatever shall be the content of the entry as a string
+ * )
+ */
+ public function getEntries() {
+ return $this->entries;
+ }
+
+ /**
+ * deletes all entries of this column. So the only way to edit an entry is
+ * getting all entries with getEntries, edit this entry, eraseEntries() and
+ * addEntries(). Not very short, but at least it works.
+ *
+ * @return CalendarColumn
+ */
+ public function eraseEntries() {
+ $this->entries = [];
+ return $this;
+ }
+
+ /**
+ * Returns an array of calendar-entries, grouped by day and additionally grouped by same start and end
+ * if groupEntries(true) has been called.
+ *
+ * @return mixed the (double-)grouped entries
+ */
+ public function getGroupedEntries()
+ {
+ if (!$this->sorted_entries) {
+ if ($this->isGrouped()) {
+ $this->sorted_entries = $this->sortAndGroupEntries();
+ } else {
+ $this->sorted_entries = $this->sortEntries();
+ }
+ }
+
+ return $this->sorted_entries;
+ }
+
+ /**
+ * sorts and groups entries and returns them
+ * only used by columns with grouped entries like instituteschedules
+ *
+ * @return array
+ */
+ public function sortAndGroupEntries()
+ {
+ $day = $this->getTitle();
+
+ $entries_for_column = $this->getEntries();
+ $result = [];
+ $new_entries = [];
+
+ // 1st step - group all entries with the same duration
+ foreach ($entries_for_column as $entry_id => $entry) {
+ $new_entries[$entry['start'] .'_'. $entry['end']][] = $entry;
+ }
+
+ $column = 0;
+
+ // 2nd step - optimize the groups
+ while (sizeof($new_entries) > 0) {
+ $lstart = 2399; $lend = 0;
+
+ foreach ($new_entries as $time => $grouped_entries) {
+ list($start, $end) = explode('_', $time);
+ if ($start < $lstart /*&& ($end - $start) >= ($lend - $lstart)*/ ) {
+ $lstart = $start;
+ $lend = $end;
+ }
+ }
+
+ $result['col_'. $column][] = $new_entries[$lstart .'_'. $lend];
+ unset($new_entries[$lstart .'_'. $lend]);
+
+ $hit = true;
+
+ while ($hit) {
+ $hit = false;
+ $hstart = 2399; $hend = 2399;
+
+ // check, if there is something, that can be placed after
+ foreach ($new_entries as $time => $grouped_entries) {
+ list($start, $end) = explode('_', $time);
+
+ if ( ($start >= $lend) && ($start < $hstart) ) {
+ $hstart = $start;
+ $hend = $end;
+ $hit = true;
+ }
+ }
+
+ if ($hit) {
+ $lend = $hend;
+ $result['col_'. $column][] = $new_entries[$hstart .'_'. $hend];
+ unset($new_entries[$hstart .'_'. $hend]);
+ }
+ }
+
+ $column++;
+ } // 2nd step
+
+ return $result;
+
+ }
+
+ /**
+ * sorts entries and returns them
+ *
+ * @return array
+ */
+ public function sortEntries()
+ {
+ $entries_for_column = $this->getEntries();
+
+ $result = [];
+ $column = 0;
+
+ // 2nd step - optimize the groups
+ while (sizeof($entries_for_column) > 0) {
+ $lstart = 2399; $lend = 0; $lkey = null;
+
+ foreach ($entries_for_column as $entry_key => $entry) {
+ if ($entry['start'] < $lstart /*&& ($end - $start) >= ($lend - $lstart)*/ ) {
+ $lstart = $entry['start'];
+ $lend = $entry['end'];
+ $lkey = $entry_key;
+ }
+ }
+
+ $result['col_'. $column][] = $entries_for_column[$lkey];
+ unset($entries_for_column[$lkey]);
+
+ $hit = true;
+
+ while ($hit) {
+ $hit = false;
+ $hstart = 2399; $hend = 2399; $hkey = null;
+
+ // check, if there is something, that can be placed after
+ foreach ($entries_for_column as $entry_key => $entry) {
+ if ( ($entry['start'] >= $lend) && ($entry['start'] < $hstart) ) {
+ // && (($end - $start) > ($hend - $hstart)) ) {
+ $hstart = $entry['start'];
+ $hend = $entry['end'];
+ $hkey = $entry_key;
+ $hit = true;
+ }
+ }
+
+ if ($hit) {
+ $lend = $hend;
+ $result['col_'. $column][] = $entries_for_column[$hkey];
+ unset($entries_for_column[$hkey]);
+ }
+ }
+
+ $column++;
+ } // 2nd step
+ return $result;
+
+ }
+
+ /**
+ * returns a matrix that tells the number of entries for a given timeslot
+ *
+ * @return array
+ */
+ public function getMatrix() {
+ $group_matrix = [];
+ foreach ($this->getGroupedEntries() as $groups) {
+ foreach ($groups as $group) {
+ if (is_array($group[0])) {
+ $data = $group[0];
+ } else {
+ $data = $group;
+ }
+
+ for ($i = floor($data['start'] / 100); $i <= floor($data['end'] / 100); $i++) {
+ for ($j = 0; $j < 60; $j++) {
+ if (($i * 100) + $j >= $data['start'] && ($i * 100) + $j < $data['end']) {
+ $group_matrix[($i * 100) + $j]++;
+ }
+ }
+ }
+ }
+ }
+ return $group_matrix;
+ }
+
+ /**
+ * check, if a grouped view of the entries is requested
+ *
+ * @return bool true if grouped, false otherwise
+ */
+ public function isGrouped()
+ {
+ return $this->grouped;
+ }
+
+ /**
+ * Call this function th enable/disable the grouping of entries with the same start and end.
+ *
+ * @param bool $group optional, defaults to true
+ * @return void
+ */
+ public function groupEntries($grouped = true)
+ {
+ $this->grouped = $grouped;
+ }
+
+} \ No newline at end of file
diff --git a/lib/calendar/CalendarExport.class.php b/lib/calendar/CalendarExport.class.php
new file mode 100644
index 0000000..c9c10f6
--- /dev/null
+++ b/lib/calendar/CalendarExport.class.php
@@ -0,0 +1,87 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * CalendarExport.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+class CalendarExport
+{
+ protected $_writer;
+ protected $export;
+ private $count;
+
+ public function __construct(&$writer)
+ {
+ $this->_writer = $writer;
+ }
+
+ public function exportFromDatabase($range_id = null, $start = 0, $end = Calendar::CALENDAR_END, $event_types = null, $except = NULL)
+ {
+ global $_calendar_error, $user;
+
+ if (!$range_id) {
+ $range_id = $user->id;
+ }
+ $calendar = new SingleCalendar($range_id);
+
+ $this->_export($this->_writer->writeHeader());
+ $calendar->getEvents($event_types, $start, $end);
+
+ foreach ($calendar->events as $event) {
+ $this->_export($this->_writer->write($event));
+ }
+ $this->count = sizeof($calendar->events);
+
+ $this->_export($this->_writer->writeFooter());
+ }
+
+ public function exportFromObjects($events)
+ {
+ global $_calendar_error;
+
+ $this->_export($this->_writer->writeHeader());
+
+ $this->count = 0;
+ foreach ($events as $event) {
+ $this->_export($this->_writer->write($event));
+ $this->count++;
+ }
+
+ if (!sizeof($events)) {
+ $message = _('Es wurden keine Termine exportiert.');
+ } else {
+ $message = sprintf(ngettext('Es wurde 1 Termin exportiert', 'Es wurden %s Termine exportiert', sizeof($events)), sizeof($events));
+ }
+
+ $this->_export($this->_writer->writeFooter());
+ }
+
+ public function _export($exp)
+ {
+ if (!empty($exp)) {
+ $this->export[] = $exp;
+ }
+ }
+
+ public function getExport()
+ {
+ return $this->export;
+ }
+
+ public function getCount()
+ {
+ return $this->count;
+ }
+}
diff --git a/lib/calendar/CalendarExportException.class.php b/lib/calendar/CalendarExportException.class.php
new file mode 100644
index 0000000..fbdabda
--- /dev/null
+++ b/lib/calendar/CalendarExportException.class.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * CalendarExportException.php - indicates an error during
+ * calendar export or import
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+*/
+
+class CalendarExportException extends Exception
+{
+}
diff --git a/lib/calendar/CalendarExportFile.class.php b/lib/calendar/CalendarExportFile.class.php
new file mode 100644
index 0000000..1e0f3fb
--- /dev/null
+++ b/lib/calendar/CalendarExportFile.class.php
@@ -0,0 +1,122 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * CalendarExportFile.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+class CalendarExportFile extends CalendarExport
+{
+ private $file_name = 'studip';
+ private $tmp_file_name;
+ private $path;
+
+ public function __construct(&$writer, $path = null, $file_name = null)
+ {
+ global $TMP_PATH;
+
+ parent::__construct($writer);
+
+ if (!$file_name) {
+ $this->tmp_file_name = $this->makeUniqueFilename();
+ $this->file_name .= '.' . $writer->getDefaultFileNameSuffix();
+ } else {
+ $this->file_name = $file_name;
+ $this->tmp_file_name = $file_name;
+ }
+
+ if (!$path) {
+ $this->path = $TMP_PATH . '/';
+ }
+
+ $this->_writer = $writer;
+ }
+
+ public function exportFromDatabase($range_id = null, $start = 0, $end = Calendar::CALENDAR_END, $event_types = null, $except = null)
+ {
+ $this->_createFile();
+ parent::exportFromDatabase($range_id, $start, $end, $event_types, $except);
+ $this->_closeFile();
+ }
+
+ public function exportFromObjects($events)
+ {
+ $this->_createFile();
+ parent::exportFromObjects($events);
+ $this->_closeFile();
+ }
+
+ public function sendFile()
+ {
+ if (file_exists($this->path . $this->tmp_file_name)) {
+ header('Location: ' . FileManager::getDownloadURLForTemporaryFile($this->tmp_file_name, $this->file_name));
+ } else {
+ throw new CalendarExportException(_('Die Export-Datei konnte nicht erstellt werden!'));
+ }
+ }
+
+ public function makeUniqueFileName()
+ {
+ return md5(uniqid(rand() . "Stud.IP Calendar"));
+ }
+
+ // returns file handle
+ public function getExport()
+ {
+ return $this->export;
+ }
+
+ public function getFileName()
+ {
+ return $this->file_name;
+ }
+
+ public function getTempFileName()
+ {
+ return $this->tmp_file_name;
+ }
+
+ public function _createFile()
+ {
+ if (!(is_dir($this->path))) {
+ if (!mkdir($this->path)) {
+ var_dump($this->path); exit;
+ throw new CalendarExportException(_('Das Export-Verzeichnis konnte nicht angelegt werden!'));
+ } else {
+ if (!chmod($this->path, 0777)) {
+ throw new CalendarExportException(_('Die Zugriffsrechte auf das Export-Verzeichnis konnten nicht geändert werden!'));
+ }
+ }
+ }
+ if (file_exists($this->path . $this->tmp_file_name)) {
+ if (!unlink($this->path . $this->tmp_file_name)) {
+ throw new CalendarExportException(_('Eine bestehende Export-Datei konnte nicht gelöscht werden!'));
+ }
+ }
+ $this->export = fopen($this->path . $this->tmp_file_name, "wb");
+ if (!$this->export) {
+ throw new CalendarExportException(_("Die Export-Datei konnte nicht erstellt werden!"));
+ }
+ }
+
+ public function _export($exp)
+ {
+ fwrite($this->export, $exp);
+ }
+
+ public function _closeFile()
+ {
+ fclose($this->export);
+ }
+}
diff --git a/lib/calendar/CalendarImport.class.php b/lib/calendar/CalendarImport.class.php
new file mode 100644
index 0000000..605d747
--- /dev/null
+++ b/lib/calendar/CalendarImport.class.php
@@ -0,0 +1,91 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * CalendarImport.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+class CalendarImport
+{
+
+ const IGNORE_ERRORS = 1;
+
+ protected $_parser;
+ private $data;
+ private $public_to_private = false;
+
+ public function __construct(&$parser, $data = null)
+ {
+ $this->_parser = $parser;
+ $this->data = $data;
+ }
+
+ public function getContent()
+ {
+ return $this->data;
+ }
+
+ public function importIntoDatabase($range_id, $ignore = CalendarImport::IGNORE_ERRORS)
+ {
+ $this->_parser->changePublicToPrivate($this->public_to_private);
+ if ($this->_parser->parseIntoDatabase($range_id, $this->getContent(), $ignore)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function importIntoObjects($ignore = CalendarImport::IGNORE_ERRORS)
+ {
+ $this->_parser->changePublicToPrivate($this->public_to_private);
+ if ($this->_parser->parseIntoObjects($this->getContent(), $ignore)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function getObjects()
+ {
+ return $objects =& $this->_parser->getObjects();
+ }
+
+ public function getCount()
+ {
+ return $this->_parser->getCount($this->getContent());
+ }
+
+ public function changePublicToPrivate($value = TRUE)
+ {
+ $this->public_to_private = $value;
+ }
+
+ public function getClientIdentifier()
+ {
+ if (!$client_identifier = $this->_parser->getClientIdentifier()) {
+ return $this->_parser->getClientIdentifier($this->getContent());
+ }
+ return $client_identifier;
+ }
+
+ public function setImportSem($do_import)
+ {
+ if ($do_import) {
+ $this->_parser->import_sem = true;
+ } else {
+ $this->_parser->import_sem = false;
+ }
+ }
+
+}
diff --git a/lib/calendar/CalendarImportFile.class.php b/lib/calendar/CalendarImportFile.class.php
new file mode 100644
index 0000000..3e2ba15
--- /dev/null
+++ b/lib/calendar/CalendarImportFile.class.php
@@ -0,0 +1,141 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * CalendarImportFile.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+class CalendarImportFile extends CalendarImport
+{
+
+ private $file;
+ private $path;
+
+ /**
+ *
+ */
+ public function __construct(&$parser, $file, $path = '')
+ {
+ parent::__construct($parser);
+ $this->file = $file;
+ $this->path = $path;
+ }
+
+ /**
+ *
+ */
+ public function getContent()
+ {
+ $data = '';
+ if (!$file = @fopen($this->file['tmp_name'], 'rb')) {
+ throw new CalendarExportException(_("Die Import-Datei konnte nicht geöffnet werden!"));
+ return false;
+ }
+ if ($file) {
+ while (!feof($file)) {
+ $data .= fread($file, 1024);
+ }
+ fclose($file);
+ }
+ return $data;
+ }
+
+ /**
+ *
+ */
+ public function getFileName()
+ {
+ return $this->file['name'];
+ }
+
+ /**
+ *
+ */
+ public function getFileType()
+ {
+ return $this->_parser->getType();
+ }
+
+ /**
+ *
+ */
+ public function getFileSize()
+ {
+ if (file_exists($this->file['tmp_name'])) {
+ return filesize($this->file['tmp_name']);
+ }
+ return false;
+ }
+
+ /**
+ *
+ */
+ public function checkFile()
+ {
+ return true;
+ }
+
+ /**
+ *
+ */
+ public function importIntoDatabase($range_id, $ignore = CalendarImport::IGNORE_ERRORS)
+ {
+ if ($this->checkFile()) {
+ parent::importIntoDatabase($range_id, $ignore);
+ return true;
+ }
+ throw new CalendarExportException(_('Die Datei konnte nicht gelesen werden!'));
+ return false;
+ }
+
+ /**
+ *
+ */
+ public function importIntoObjects($ignore = CalendarImport::IGNORE_ERRORS)
+ {
+ global $_calendar_error;
+
+ if ($this->checkFile()) {
+ parent::importIntoObjects($ignore);
+ return true;
+ }
+ throw new CalendarExportException(_('Die Datei konnte nicht gelesen werden!'));
+ }
+
+ /**
+ *
+ */
+ public function deleteFile()
+ {
+ if (!unlink($this->file['tmp_name'])) {
+ throw new CalendarExportException(_("Die Datei konnte nicht gelöscht werden!"));
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ *
+ */
+ public function _getFileExtension()
+ {
+ $i = mb_strrpos($this->file['name'], '.');
+ if (!$i) {
+ return '';
+ }
+ $l = mb_strlen($this->file['name']) - $i;
+ $ext = mb_substr($this->file['name'], $i + 1, $l);
+ return $ext;
+ }
+}
diff --git a/lib/calendar/CalendarParser.class.php b/lib/calendar/CalendarParser.class.php
new file mode 100644
index 0000000..07e556f
--- /dev/null
+++ b/lib/calendar/CalendarParser.class.php
@@ -0,0 +1,132 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * CalendarParser.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+class CalendarParser
+{
+ private $events = [];
+ protected $components;
+ private $type;
+ private $number_of_events;
+ protected $public_to_private = false;
+ protected $client_identifier;
+ private $time;
+
+ public function __construct()
+ {
+ $this->client_identifier = '';
+ }
+
+ public function parse($data, $ignore = null)
+ {
+ foreach ($data as $properties) {
+ if ($this->public_to_private && $properties['CLASS'] == 'PUBLIC') {
+ $properties['CLASS'] = 'PRIVATE';
+ }
+ $properties['CATEGORIES'] = implode(', ', $properties['CATEGORIES']);
+ $this->components[] = $properties;
+ }
+ }
+
+ public function getCount($data)
+ {
+ return 0;
+ }
+
+ public function parseIntoDatabase($range_id, $data, $ignore)
+ {
+ if ($this->parseIntoObjects($range_id, $data, $ignore)) {
+ foreach ($this->events as $event) {
+ $event->store();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ public function parseIntoObjects($range_id, $data, $ignore)
+ {
+ $this->time = time();
+ if ($this->parse($data, $ignore)) {
+ if (is_array($this->components)) {
+ foreach ($this->components as $component) {
+ $calendar_event = CalendarEvent::findByUid($component['UID'], $range_id);
+ if ($calendar_event) {
+ $this->setProperties($calendar_event, $component);
+ $calendar_event->setRecurrence($component['RRULE']);
+ $this->events[] = $calendar_event;
+ } else {
+ $calendar_event = new CalendarEvent();
+ $event = new EventData();
+ $event->author_id = $GLOBALS['user']->id;
+ $event->event_id = $event->getNewId();
+ $event->uid = $component['UID'];
+ $calendar_event->range_id = $range_id;
+ $calendar_event->event_id = $event->event_id;
+ $calendar_event->event = $event;
+ $this->setProperties($calendar_event, $component);
+ $calendar_event->setRecurrence($component['RRULE']);
+ $this->events[] = $calendar_event;
+ }
+ }
+ }
+ return true;
+ }
+ $message = _('Die Import-Daten konnten nicht verarbeitet werden!');
+
+ return false;
+ }
+
+ private function setProperties($calendar_event, $component)
+ {
+ $calendar_event->setStart($component['DTSTART']);
+ $calendar_event->setEnd($component['DTEND']);
+ $calendar_event->setTitle($component['SUMMARY']);
+ $calendar_event->event->description = $component['DESCRIPTION'];
+ $calendar_event->setAccessibility($component['CLASS']);
+ $calendar_event->setUserDefinedCategories($component['CATEGORIES']);
+ $calendar_event->event->category_intern = $component['STUDIP_CATEGORY'] ?: 1;
+ $calendar_event->setPriority($component['PRIORITY']);
+ $calendar_event->event->location = $component['LOCATION'];
+ $calendar_event->setExceptions($component['EXDATE']);
+ $calendar_event->event->mkdate = $component['CREATED'];
+ $calendar_event->event->chdate = $component['LAST-MODIFIED'] ?: $component['CREATED'];
+ $calendar_event->event->importdate = $this->time;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function &getObjects()
+ {
+ return $objects =& $this->events;
+ }
+
+ public function changePublicToPrivate($value = true)
+ {
+ $this->public_to_private = $value;
+ }
+
+ public function getClientIdentifier($data = null)
+ {
+ return $this->client_identifier;
+ }
+}
+
diff --git a/lib/calendar/CalendarParserICalendar.class.php b/lib/calendar/CalendarParserICalendar.class.php
new file mode 100644
index 0000000..241580a
--- /dev/null
+++ b/lib/calendar/CalendarParserICalendar.class.php
@@ -0,0 +1,632 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * CalendarParserICalendar.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+class CalendarParserICalendar extends CalendarParser
+{
+ public $type = '';
+ protected $count = null;
+
+ public function __construct()
+ {
+ parent::__construct();
+ $this->type = 'iCalendar';
+ // initialize error handler
+ $GLOBALS['_calendar_error'] = new ErrorHandler();
+ }
+
+ public function getCount($data)
+ {
+ $matches = [];
+ if (is_null($this->count)) {
+ // Unfold any folded lines
+ $data = preg_replace('/\x0D?\x0A[\x20\x09]/', '', $data);
+ preg_match_all('/(BEGIN:VEVENT(\r\n|\r|\n)[\W\w]*?END:VEVENT\r?\n?)/', $data, $matches);
+ $this->count = sizeof($matches[1]);
+ }
+
+ return $this->count;
+ }
+
+ /**
+ * Parse a string containing vCalendar data.
+ *
+ * @access private
+ * @param String $data The data to parse
+ *
+ */
+ public function parse($data, $ignore = null)
+ {
+ global $_calendar_error, $PERS_TERMIN_KAT;
+
+ // match categories
+ $studip_categories = [];
+ $i = 1;
+ foreach ($PERS_TERMIN_KAT as $cat) {
+ $studip_categories[mb_strtolower($cat['name'])] = $i++;
+ }
+
+ // Unfold any folded lines
+ // the CR is optional for files imported from Korganizer (non-standard)
+ $data = preg_replace('/\x0D?\x0A[\x20\x09]/', '', $data);
+
+ if (!preg_match('/BEGIN:VCALENDAR(\r\n|\r|\n)([\W\w]*)END:VCALENDAR\r?\n?/', $data, $matches)) {
+ $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Import-Datei ist keine gültige iCalendar-Datei!"));
+ return false;
+ }
+
+ // client identifier
+ if (!$this->_parseClientIdentifier($matches[2])) {
+ return false;
+ }
+
+ // All sub components
+ if (!preg_match_all('/BEGIN:VEVENT(\r\n|\r|\n)([\w\W]*?)END:VEVENT(\r\n|\r|\n)/', $matches[2], $v_events)) {
+ $_calendar_error->throwError(ErrorHandler::ERROR_MESSAGE, _("Die importierte Datei enthält keine Termine."));
+ return true;
+ }
+
+ if ($this->count) {
+ $this->count = 0;
+ }
+ foreach ($v_events[2] as $v_event) {
+ $properties['CLASS'] = 'PRIVATE';
+ // Parse the remain attributes
+
+ if (preg_match_all('/(.*):(.*)(\r|\n)+/', $v_event, $matches)) {
+ $properties = [];
+ $check = [];
+ foreach ($matches[0] as $property) {
+ preg_match('/([^;^:]*)((;[^:]*)?):(.*)/', $property, $parts);
+ $tag = $parts[1];
+ $value = $parts[4];
+ $params = [];
+
+ // skip seminar events
+ if ((!$this->import_sem) && $tag == 'UID') {
+ if (mb_strpos($value, 'Stud.IP-SEM') === 0) {
+ continue 2;
+ }
+ }
+
+ if (!empty($parts[2])) {
+ preg_match_all('/;(([^;=]*)(=([^;]*))?)/', $parts[2], $param_parts);
+ foreach ($param_parts[2] as $key => $param_name)
+ $params[mb_strtoupper($param_name)] = mb_strtoupper($param_parts[4][$key]);
+
+ if ($params['ENCODING']) {
+ switch ($params['ENCODING']) {
+ case 'QUOTED-PRINTABLE':
+ $value = $this->_qp_decode($value);
+ break;
+
+ case 'BASE64':
+ $value = base64_decode($value);
+ break;
+ }
+ }
+ }
+
+ switch ($tag) {
+ // text fields
+ case 'DESCRIPTION':
+ case 'SUMMARY':
+ case 'LOCATION':
+ $value = preg_replace('/\\\\,/', ',', $value);
+ $value = preg_replace('/\\\\n/', "\n", $value);
+ $properties[$tag] = trim($value);
+ break;
+
+ case 'CATEGORIES':
+ $categories = [];
+ $properties['STUDIP_CATEGORY'] = null;
+ foreach (explode(',', $value) as $category) {
+ if (!$studip_categories[mb_strtolower($category)]) {
+ $categories[] = $category;
+ } else if (!$properties['STUDIP_CATEGORY']) {
+ $properties['STUDIP_CATEGORY']
+ = $studip_categories[mb_strtolower($category)];
+ }
+ }
+ $properties[$tag] = implode(',', $categories);
+ break;
+
+ // Date fields
+ case 'DCREATED': // vCalendar property name for "CREATED"
+ $tag = "CREATED";
+ case 'DTSTAMP':
+ case 'COMPLETED':
+ case 'CREATED':
+ case 'LAST-MODIFIED':
+ $properties[$tag] = $this->_parseDateTime($value);
+ break;
+
+ case 'DTSTART':
+ case 'DTEND':
+ // checking for day events
+ if ($params['VALUE'] == 'DATE')
+ $check['DAY_EVENT'] = true;
+ case 'DUE':
+ case 'RECURRENCE-ID':
+ $properties[$tag] = $this->_parseDateTime($value);
+ break;
+
+ case 'RDATE':
+ if (array_key_exists('VALUE', $params)) {
+ if ($params['VALUE'] == 'PERIOD') {
+ $properties[$tag] = $this->_parsePeriod($value);
+ } else {
+ $properties[$tag] = $this->_parseDateTime($value);
+ }
+ } else {
+ $properties[$tag] = $this->_parseDateTime($value);
+ }
+ break;
+
+ case 'TRIGGER':
+ if (array_key_exists('VALUE', $params)) {
+ if ($params['VALUE'] == 'DATE-TIME') {
+ $properties[$tag] = $this->_parseDateTime($value);
+ } else {
+ $properties[$tag] = $this->_parseDuration($value);
+ }
+ } else {
+ $properties[$tag] = $this->_parseDuration($value);
+ }
+ break;
+
+ case 'EXDATE':
+ $properties[$tag] = [];
+ // comma seperated dates
+ $values = [];
+ $dates = [];
+ preg_match_all('/,([^,]*)/', ',' . $value, $values);
+ foreach ($values[1] as $value) {
+ if (array_key_exists('VALUE', $params)) {
+ if ($params['VALUE'] == 'DATE-TIME') {
+ $dates[] = $this->_parseDateTime($value);
+ } else if ($params['VALUE'] == 'DATE') {
+ $dates[] = $this->_parseDate($value);
+ }
+ } else {
+ $dates[] = $this->_parseDateTime($value);
+ }
+ }
+ // some iCalendar exports (e.g. KOrganizer) use an EXDATE-entry for every
+ // exception, so we have to merge them
+ array_merge($properties[$tag], $dates);
+ break;
+
+ // Duration fields
+ case 'DURATION':
+ $attibutes[$tag] = $this->_parseDuration($value);
+ break;
+
+ // Period of time fields
+ case 'FREEBUSY':
+ $values = [];
+ $periods = [];
+ preg_match_all('/,([^,]*)/', ',' . $value, $values);
+ foreach ($values[1] as $value) {
+ $periods[] = $this->_parsePeriod($value);
+ }
+
+ $properties[$tag] = $periods;
+ break;
+
+ // UTC offset fields
+ case 'TZOFFSETFROM':
+ case 'TZOFFSETTO':
+ $properties[$tag] = $this->_parseUtcOffset($value);
+ break;
+
+ case 'PRIORITY':
+ $properties[$tag] = $this->_parsePriority($value);
+ break;
+
+ case 'CLASS':
+ switch (trim($value)) {
+ case 'PUBLIC':
+ $properties[$tag] = 'PUBLIC';
+ break;
+ case 'CONFIDENTIAL':
+ $properties[$tag] = 'CONFIDENTIAL';
+ break;
+ default:
+ $properties[$tag] = 'PRIVATE';
+ }
+ break;
+
+ // Integer fields
+ case 'PERCENT-COMPLETE':
+ case 'REPEAT':
+ case 'SEQUENCE':
+ $properties[$tag] = intval($value);
+ break;
+
+ // Geo fields
+ case 'GEO':
+ $floats = explode(';', $value);
+ $value['latitude'] = floatval($floats[0]);
+ $value['longitude'] = floatval($floats[1]);
+ $properties[$tag] = $value;
+ break;
+
+ // Recursion fields
+ case 'EXRULE':
+ case 'RRULE':
+ $properties[$tag] = $this->_parseRecurrence($value);
+ break;
+
+ default:
+ // string fields
+ $properties[$tag] = trim($value);
+ break;
+ }
+ }
+
+ if (!$properties['RRULE']['rtype'])
+ $properties['RRULE'] = ['rtype' => 'SINGLE'];
+
+ if (!$properties['LAST-MODIFIED'])
+ $properties['LAST-MODIFIED'] = $properties['CREATED'];
+
+ if (!$properties['DTSTART'] || ($properties['EXDATE'] && !$properties['RRULE'])) {
+ $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!"));
+ $this->count = 0;
+ return false;
+ }
+
+ if (!$properties['DTEND'])
+ $properties['DTEND'] = $properties['DTSTART'];
+
+ // day events starts at 00:00:00 and ends at 23:59:59
+ if ($check['DAY_EVENT'])
+ $properties['DTEND']--;
+
+ // default: all imported events are set to private
+ if (!$properties['CLASS']
+ || ($this->public_to_private && $properties['CLASS'] == 'PUBLIC')) {
+ $properties['CLASS'] = 'PRIVATE';
+ }
+
+ /*
+ if (isset($studip_categories[$properties['CATEGORIES']])) {
+ $properties['STUDIP_CATEGORY'] = $studip_categories[$properties['CATEGORIES']];
+ $properties['CATEGORIES'] = '';
+ }
+ *
+ */
+
+ $this->components[] = $properties;
+ } else {
+ $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!"));
+ $this->count = 0;
+ return false;
+ }
+ $this->count++;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse a UTC Offset field
+ */
+ private function _parseUtcOffset($text)
+ {
+ $offset = 0;
+ if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $matches)) {
+ $offset += 3600 * intval($matches[2]);
+ $offset += 60 * intval($matches[3]);
+ $offset *= ( $matches[1] == '+' ? 1 : -1);
+ if (array_key_exists(4, $matches)) {
+ $offset += intval($matches[4]);
+ }
+ return $offset;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Parse a Time Period field
+ */
+ private function _parsePeriod($text)
+ {
+ $matches = explode('/', $text);
+
+ $start = $this->_parseDateTime($matches[0]);
+
+ if ($duration = $this->_parseDuration($matches[1])) {
+ return ['start' => $start, 'duration' => $duration];
+ } else if ($end = $this->_parseDateTime($matches[1])) {
+ return ['start' => $start, 'end' => $end];
+ }
+ }
+
+ /**
+ * Parse a DateTime field
+ */
+ private function _parseDateTime($text)
+ {
+ $dateParts = explode('T', $text);
+ if (count($dateParts) != 2 && !empty($text)) {
+ // not a date time field but may be just a date field
+ if (!$date = $this->_parseDate($text)) {
+ return $date;
+ }
+ $date = $this->_parseDate($text);
+ return mktime(0, 0, 0, $date['month'], $date['mday'], $date['year']);
+ }
+
+ if (!$date = $this->_parseDate($dateParts[0])) {
+ return $date;
+ }
+ if (!$time = $this->_parseTime($dateParts[1])) {
+ return $time;
+ }
+
+ if ($time['zone'] == 'UTC') {
+ return gmmktime($time['hour'], $time['minute'], $time['second'], $date['month'], $date['mday'], $date['year']);
+ } else {
+ return mktime($time['hour'], $time['minute'], $time['second'], $date['month'], $date['mday'], $date['year']);
+ }
+ }
+
+ /**
+ * Parse a Time field
+ */
+ private function _parseTime($text)
+ {
+ if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $matches)) {
+ $time['hour'] = intval($matches[1]);
+ $time['minute'] = intval($matches[2]);
+ $time['second'] = intval($matches[3]);
+ if (array_key_exists(4, $matches)) {
+ $time['zone'] = 'UTC';
+ } else {
+ $time['zone'] = 'LOCAL';
+ }
+ return $time;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Parse a Date field
+ */
+ private function _parseDate($text)
+ {
+ if (mb_strlen(trim($text)) !== 8) {
+ return false;
+ }
+
+ $date['year'] = intval(mb_substr($text, 0, 4));
+ $date['month'] = intval(mb_substr($text, 4, 2));
+ $date['mday'] = intval(mb_substr($text, 6, 2));
+
+ return $date;
+ }
+
+ /**
+ * Parse a Duration Value field
+ */
+ private function _parseDuration($text)
+ {
+ if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $matches)) {
+ // weeks
+ $duration = 7 * 86400 * intval($matches[3]);
+ if (count($matches) > 4) {
+ // days
+ $duration += 86400 * intval($matches[4]);
+ }
+ if (count($matches) > 5) {
+ // hours
+ $duration += 3600 * intval($matches[7]);
+ // mins
+ if (array_key_exists(8, $matches)) {
+ $duration += 60 * intval($matches[8]);
+ }
+ // secs
+ if (array_key_exists(9, $matches)) {
+ $duration += intval($matches[9]);
+ }
+ }
+ // sign
+ if ($matches[1] == "-") {
+ $duration *= - 1;
+ }
+
+ return $duration;
+ } else {
+ return false;
+ }
+ }
+
+ private function _parsePriority($value)
+ {
+ $value = intval($value);
+ if ($value > 0 && $value < 5) {
+ return 1;
+ }
+
+ if ($value == 5) {
+ return 2;
+ }
+
+ if ($value > 5 && $value < 10) {
+ return 3;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Parse a Recurrence field
+ */
+ private function _parseRecurrence($text)
+ {
+ global $_calendar_error;
+
+ if (preg_match_all('/([A-Za-z]*?)=([^;]*);?/', $text, $matches, PREG_SET_ORDER)) {
+ $r_rule = [];
+
+ foreach ($matches as $match) {
+ switch ($match[1]) {
+ case 'FREQ' :
+ switch (trim($match[2])) {
+ case 'DAILY' :
+ case 'WEEKLY' :
+ case 'MONTHLY' :
+ case 'YEARLY' :
+ $r_rule['rtype'] = trim($match[2]);
+ break;
+ default:
+ $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann."));
+ break;
+ }
+ break;
+
+ case 'UNTIL' :
+ $r_rule['expire'] = $this->_parseDateTime($match[2]);
+ break;
+
+ case 'COUNT' :
+ $r_rule['count'] = intval($match[2]);
+ break;
+
+ case 'INTERVAL' :
+ $r_rule['linterval'] = intval($match[2]);
+ break;
+
+ case 'BYSECOND' :
+ case 'BYMINUTE' :
+ case 'BYHOUR' :
+ case 'BYWEEKNO' :
+ case 'BYYEARDAY' :
+ $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann."));
+ break;
+
+ case 'BYDAY' :
+ $byday = $this->_parseByDay($match[2]);
+ $r_rule['wdays'] = $byday['wdays'];
+ if ($byday['sinterval'])
+ $r_rule['sinterval'] = $byday['sinterval'];
+ break;
+
+ case 'BYMONTH' :
+ $r_rule['month'] = $this->_parseByMonth($match[2]);
+ break;
+
+ case 'BYMONTHDAY' :
+ $r_rule['day'] = $this->_parseByMonthDay($match[2]);
+ break;
+
+ case 'BYSETPOS':
+ $r_rule['sinterval'] = intval($match[2]);
+ break;
+
+ case 'WKST' :
+ break;
+ }
+ }
+ }
+
+ return $r_rule;
+ }
+
+ private function _parseByDay($text)
+ {
+ global $_calendar_error;
+
+ preg_match_all('/(-?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU),?/', $text, $matches, PREG_SET_ORDER);
+ $wdays_map = ['MO' => '1', 'TU' => '2', 'WE' => '3', 'TH' => '4', 'FR' => '5',
+ 'SA' => '6', 'SU' => '7'];
+ $wdays = "";
+ $sinterval = null;
+ foreach ($matches as $match) {
+ $wdays .= $wdays_map[$match[2]];
+ if ($match[1]) {
+ if (!$sinterval && ((int) $match[1]) > 0 || $match[1] == '-1') {
+ if ($match[1] == '-1')
+ $sinterval = '5';
+ else
+ $sinterval = $match[1];
+ } else {
+ $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann."));
+ }
+ }
+ }
+
+ return $wdays ? ['wdays' => $wdays, 'sinterval' => $sinterval] : false;
+ }
+
+ private function _parseByMonthDay($text)
+ {
+ $days = explode(',', $text);
+ if (sizeof($days) > 1 || ((int) $days[0]) < 0) {
+ return false;
+ }
+
+ return $days[0];
+ }
+
+ private function _parseByMonth($text)
+ {
+ $months = explode(',', $text);
+ if (sizeof($months) > 1) {
+ return false;
+ }
+
+ return $months[0];
+ }
+
+ private function _qp_decode($value)
+ {
+ return preg_replace_callback("/=([0-9A-F]{2})/", function ($m) {return chr(hexdec($m[1]));}, $value);
+ }
+
+ private function _parseClientIdentifier(&$data)
+ {
+ global $_calendar_error;
+
+ if ($this->client_identifier == '') {
+ if (!preg_match('/PRODID((;[\W\w]*)*):([\W\w]+?)(\r\n|\r|\n)/', $data, $matches)) {
+ $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!"));
+ return false;
+ } elseif (!trim($matches[3])) {
+ $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!"));
+ return false;
+ } else {
+ $this->client_identifier = trim($matches[3]);
+ }
+ }
+ return true;
+ }
+
+ public function getClientIdentifier($data = null)
+ {
+ if (!is_null($data)) {
+ $this->_parseClientIdentifier($data);
+ }
+
+ return $this->client_identifier;
+ }
+
+}
diff --git a/lib/calendar/CalendarView.class.php b/lib/calendar/CalendarView.class.php
new file mode 100644
index 0000000..0b8f905
--- /dev/null
+++ b/lib/calendar/CalendarView.class.php
@@ -0,0 +1,340 @@
+<?php
+# Lifter010: TODO
+
+ /**
+ * CalendarView.class.php - generates a calendar
+ *
+ * This class takes and checks all necessary parameters to display a calendar/schedule/time-table.
+ *
+ * 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.
+ *
+ * @author Rasmus Fuhse <fuhse@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+
+/**
+ * Kind of bean class for the calendar view.
+ *
+ * Example of use:
+ *
+ * // create a calendar-view and add a column
+ * $plan = new CalendarView();
+ * $plan->addColumn(_('Spalte 1'))
+ * ->addEntry(array(
+ * 'id' => 1,
+ * 'color' => '#5C2D64',
+ * 'start' => '0930',
+ * 'end' => '1100',
+ * 'title' => 'Mathe 2',
+ * 'content' => 'Die Mathematiker kreiden sich mal wieder was an.'
+ * )
+ * );
+ *
+ * // display the calendar (containing one column)
+ * print $plan->render();
+ *
+ * @since 2.0
+ */
+
+class CalendarView
+{
+
+ protected $entries = [];
+ protected $entry_columns = [];
+ protected $height = 40;
+ protected $grouped = false;
+ protected $start_hour = 8;
+ protected $end_hour = 21;
+ protected $insertFunction = "";
+ protected $templates = [];
+ protected $read_only = false;
+
+ static protected $number_of_instances = 1;
+ protected $view_id;
+
+
+ /**
+ * You need to pass an instance of this class to the template. The constructor
+ * expects an array of entries of the following type:
+ * array(
+ * $day_number => array(array (
+ * 'color' => the color in hex (css-like, without the #)
+ * 'start' => the (start hour * 100) + (start minute)
+ * 'end' => the (end hour * 100) + (end minute)
+ * //'day' => day of week (0 = Sunday, ... , 6 = Saturday)
+ * 'title' => the entry`s title
+ * 'content' => whatever shall be the content of the entry as a string
+ * ) ...) ...
+ * )
+ *
+ * @param mixed $entries an array of entries (see above)
+ * @param string $controller the name of the controller. Used to create links.
+ */
+ public function __construct($entries = [])
+ {
+ if (!is_array($entries)) {
+ throw new Exception('You need to pass some entries to the CalendarView!');
+ }
+ $this->view_id = self::$number_of_instances++;
+ $this->checkEntries($entries);
+ $this->entries = $entries;
+ }
+
+ /**
+ * set the height for one hour. This value is used to calculate the whole height of the schedule.
+ *
+ * @param int $entry_height the height of one hour in the schedule
+ */
+ public function setHeight($height)
+ {
+ $this->height = $height;
+ }
+
+ /**
+ * set the range of hours to be displayed. the start_hour has to be smaller than the end_hour
+ *
+ * @param int $start_hour the hour to start displaying at
+ * @param int $end_hour the hour to stop displaying at
+ */
+ public function setRange($start_hour, $end_hour)
+ {
+ $this->start_hour = $start_hour;
+ $this->end_hour = $end_hour;
+ }
+
+ /**
+ * does some plausability checks on an array of calendar-entries
+ *
+ * @param mixed $entries an array of calendar-entries
+ *
+ * @return bool false if check failed, true otherwise
+ */
+ protected function checkEntries($entries)
+ {
+ foreach ($entries as $column) {
+ if (!$column instanceof CalendarColumn) {
+ throw new Exception('A column of the entries in the CalenarView is not of type CalendarColumn.');
+ }
+ }
+ return true;
+ }
+
+ /**
+ * adds a new column to this view. All entries created with addEntry will be
+ * added to this column.
+ *
+ * @param string $title like "monday" to be displayed on top of the column
+ * @param string $url to be called when clicked on the title of the column
+ * @param string $id any kind of id of the column
+ * @return CalendarView
+ */
+ public function addColumn($title, $url = "", $id = null)
+ {
+ $this->entries[] = CalendarColumn::create($id)
+ ->setTitle($title)
+ ->setURL($url);
+ return $this;
+ }
+
+
+ /**
+ * adds a new entry to the last current column. The entry needs to be an
+ * associative array with parameters as follows:
+ * @param array $entry_array: associative array for an entry in the column like
+ * array (
+ * 'color' => the color in hex (css-like, without the #)
+ * 'start' => the (start hour * 100) + (start minute)
+ * 'end' => the (end hour * 100) + (end minute)
+ * 'title' => the entry`s title
+ * 'content' => whatever shall be the content of the entry as a string
+ * )
+ * @return CalendarView
+ */
+ public function addEntry($entry_array)
+ {
+ if (count($this->entries)) {
+ $this->entries[count($this->entries)-1]->addEntry($entry_array);
+ } else {
+ throw new InvalidArgumentException(_("Es existiert noch keine Spalte in der Ansicht, zu der der Eintrag hinzugefügt werden kann."));
+ }
+ return $this;
+ }
+
+
+ /**
+ * Call this function to enable/disable the grouping of entries with the same start and end.
+ *
+ * @param bool $group optional, defaults to true
+ */
+ public function groupEntries($grouped = true)
+ {
+ $this->grouped = $grouped;
+ foreach($this->getColumns() as $entry_column) {
+ $entry_column->groupEntries();
+ }
+ }
+
+ /**
+ * When a column is clicked at no entry this function is called.
+ * First the templates generates a new entry at the clicked time. Then this
+ * js-function is called which gets the parameters
+ * "function (new_entry_dom_object, column_id, hour) { ... }"
+ * with new_entry_dom_object: a real dom-object of the div of the entry
+ * column_id: id of the column
+ * hour: integer number from 0 to 23
+ * If js_function_object is an empty string, nothing will be done.
+ *
+ * @param string $js_function_object name of js-function or anonymous js-function
+ * @return CalendarView
+ */
+ public function setInsertFunction($js_function_object)
+ {
+ $this->insertFunction = $js_function_object;
+ return $this;
+ }
+
+ /**
+ * outputs the CalendarView with all (grouped) dates in columns.
+ *
+ * @param array $params you can pass some additional variables to the templates
+ *
+ * @return string
+ */
+ public function render($params = [])
+ {
+ $style_parameters = [
+ 'whole_height' => $this->getOverallHeight(),
+ 'entry_height' => $this->getHeight()
+ ];
+ $factory = new Flexi_TemplateFactory(dirname(__file__).'/../../app/views');
+ PageLayout::addStyle($factory->render('calendar/stylesheet', $style_parameters));
+
+ $template = $GLOBALS['template_factory']->open("calendar/calendar_view.php");
+ $template->set_attribute("calendar_view", $this);
+ $template->set_attribute("view_id", $this->view_id);
+ return $template->render($params);
+ }
+
+
+ /* * * * * * * * * * * * * * *
+ * * * G E T T E R S * * *
+ * * * * * * * * * * * * * * */
+
+ /**
+ * Returns an array of calendar-entries, grouped by day and additionally grouped by same start and end
+ * if groupEntries(true) has been called.
+ *
+ * @return mixed the (double-)grouped entries
+ */
+ public function getEntries()
+ {
+ $this->sorted_entries = [];
+ foreach ($this->getColumns() as $entry_column) {
+ $this->sorted_entries['day_'. $entry_column->getId()] = $entry_column->getGroupedEntries();
+ }
+ return $this->sorted_entries;
+ }
+
+ /**
+ * Returns an array where for each hour the number of concurrent entries is denoted.
+ * Used by the calendar to display the entries in parallel.
+ *
+ * @return mixed concurrent entries at each hour
+ */
+ public function getMatrix()
+ {
+ $matrix = [];
+ foreach ($this->getColumns() as $day => $entry_column) {
+ $matrix['day_'.$day] = $entry_column->getMatrix();
+ }
+ return $matrix;
+ }
+
+
+ /**
+ * returns the previously set start- and end-hour, denoting the
+ * range of entries to be displayed in the current calendar-view
+ *
+ * @return array consisting of the start and end hour
+ */
+ public function getRange()
+ {
+ return [$this->start_hour, $this->end_hour];
+ }
+
+ /**
+ * the calendar can be used in two modes. Use this function to check,
+ * if the grouping-mode is enabled for the current calendar-view
+ *
+ * @return bool true if grouped, false otherwise
+ */
+ public function isGrouped()
+ {
+ return $this->grouped;
+ }
+
+ /**
+ * returns the previously set height for one hour
+ *
+ * @return mixed the height
+ */
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * returns the overall height of the calendar
+ *
+ * @return mixed the overall height
+ */
+ public function getOverallHeight()
+ {
+ return $this->height * ($this->end_hour - $this->start_hour) + $this->height
+ + 2 + ($this->end_hour - $this->start_hour) * 2;
+ }
+
+ /**
+ * returns the previously set javasscript insert-function
+ *
+ * @return string name of js-function or anonymous js-function
+ */
+ public function getInsertFunction() {
+ return $this->insertFunction;
+ }
+
+ /**
+ * returns all columns of the calendar-view
+ *
+ * @return array of CalendarColumn
+ */
+ public function getColumns() {
+ return $this->entries;
+ }
+
+ /**
+ * Set the read-only status of the calendar
+ *
+ * @param bool $readonly true to make it read only, false otherwise
+ *
+ * @return void
+ */
+ public function setReadOnly($readonly = true)
+ {
+ $this->read_only = $readonly;
+ }
+
+ /**
+ * Return the read-only status of this calendar
+ *
+ * @return bool the read-only status of this calendar
+ */
+ public function getReadOnly() {
+ return $this->read_only;
+ }
+
+}
diff --git a/lib/calendar/CalendarWeekView.class.php b/lib/calendar/CalendarWeekView.class.php
new file mode 100644
index 0000000..e9455ea
--- /dev/null
+++ b/lib/calendar/CalendarWeekView.class.php
@@ -0,0 +1,123 @@
+<?php
+# Lifter010: TODO
+
+/**
+ * CalendarWeekView.class.php - a specialized calendar view for displaying weeks
+ *
+ * This class takes and checks all necessary parameters to display a calendar/schedule/time-table.
+ *
+ * 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.
+ *
+ * @author Till Glöggler <tgloeggl@uos.de> & Rasmus Fuhse <fuhse@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+
+/**
+ * Kind of bean class for the calendar view.
+ *
+ * @since 2.0
+ */
+
+class CalendarWeekView extends CalendarView
+{
+ protected $days = [1,2,3,4,5];
+ protected static $day_names = ["Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","Sonntag"];
+
+
+
+ /**
+ * You need to pass an instance of this class to the template. The constructor
+ * expects an array of entries of the following type:
+ * array(
+ * $day_number => array(array (
+ * 'color' => the color in hex (css-like, without the #)
+ * 'start' => the (start hour * 100) + (start minute)
+ * 'end' => the (end hour * 100) + (end minute)
+ * //'day' => day of week (0 = Sunday, ... , 6 = Saturday)
+ * 'title' => the entry`s title
+ * 'content' => whatever shall be the content of the entry as a string
+ * ) ...) ...
+ * )
+ *
+ * @param mixed $entries an array of entries (see above)
+ * @param string $controller the name of the controller. Used to create links.
+ */
+ public function __construct($entries, $controller)
+ {
+ parent::__construct($entries);
+ $this->context = $controller;
+ }
+
+ /**
+ * Call this function th enable/disable the grouping of entries with the same start and end.
+ *
+ * @param bool $group optional, defaults to true
+ */
+ public function groupEntries($grouped = true)
+ {
+ $this->grouped = $grouped;
+ foreach($this->entries as $entry_column) {
+ $entry_column->groupEntries();
+ }
+ }
+
+ /* * * * * * * * * * * * * * *
+ * * * G E T T E R S * * *
+ * * * * * * * * * * * * * * */
+
+ /**
+ * @return mixed the context
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * @return mixed the days
+ */
+ public function getDays()
+ {
+ return $this->days;
+ }
+
+ /**
+ * returns the previously set javasscript insert-function only
+ * if read_only is not set.
+ *
+ * @return string name of js-function or anonymous js-function
+ */
+ public function getInsertFunction() {
+ if (!$this->read_only) {
+ return parent::getInsertFunction();
+ }
+
+ return false;
+ }
+
+ /**
+ * returns all columns of the calendar-view nad removes the url if
+ * read_only is set
+ *
+ * @return array of CalendarColumn
+ */
+ public function getColumns() {
+ // remove links and urls if calendar-view is read-only
+ if ($this->read_only) {
+ foreach ($this->entries as $column) {
+ $column->setURL(false);
+ foreach ($column->entries as $key => $entry) {
+ unset($column->entries[$key]['url']);
+ unset($column->entries[$key]['onClick']);
+ unset($column->entries[$key]['icons']);
+ }
+ }
+ }
+
+ return parent::getColumns();
+ }
+}
diff --git a/lib/calendar/CalendarWriter.class.php b/lib/calendar/CalendarWriter.class.php
new file mode 100644
index 0000000..941c123
--- /dev/null
+++ b/lib/calendar/CalendarWriter.class.php
@@ -0,0 +1,53 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * CalendarWriter.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+class CalendarWriter
+{
+ var $default_filename_suffix;
+ var $format;
+ var $client_identifier;
+
+ public function __construct()
+ {
+ // initialize error handler
+ $GLOBALS['_calendar_error'] = new ErrorHandler();
+ }
+
+ public function write(Event &$event)
+ {
+ return $event->properties;
+ }
+
+ public function writeHeader()
+ {
+ }
+
+ public function writeFooter()
+ {
+ }
+
+ public function getDefaultFilenameSuffix()
+ {
+ return $this->default_filename_suffix;
+ }
+
+ public function getFormat()
+ {
+ return $this->format;
+ }
+}
diff --git a/lib/calendar/CalendarWriterICalendar.class.php b/lib/calendar/CalendarWriterICalendar.class.php
new file mode 100644
index 0000000..fdc32b3
--- /dev/null
+++ b/lib/calendar/CalendarWriterICalendar.class.php
@@ -0,0 +1,628 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * CalendarWriterICalendar.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+define('CALENDAR_WEEKSTART', 'MO');
+
+class CalendarWriteriCalendar extends CalendarWriter
+{
+ var $newline = "\r\n";
+
+ public function __construct()
+ {
+
+ parent::__construct();
+ $this->default_filename_suffix = "ics";
+ $this->format = "iCalendar";
+ }
+
+ public function writeHeader()
+ {
+
+ // Default values
+ $header = "BEGIN:VCALENDAR" . $this->newline;
+ $header .= "VERSION:2.0" . $this->newline;
+ if ($this->client_identifier) {
+ $header .= "PRODID:" . $this->client_identifier . $this->newline;
+ } else {
+ $header .= "PRODID:-//Stud.IP@{$_SERVER['SERVER_NAME']}//Stud.IP_iCalendar Library";
+ $header .= " //EN" . $this->newline;
+ }
+ $header .= "METHOD:PUBLISH" . $this->newline;
+
+ // time zone definition CET/CEST
+ $header .= 'CALSCALE:GREGORIAN' . $this->newline
+ . 'BEGIN:VTIMEZONE' . $this->newline
+ . 'TZID:Europe/Berlin' . $this->newline
+ . 'BEGIN:DAYLIGHT' . $this->newline
+ . 'TZOFFSETFROM:+0100' . $this->newline
+ . 'TZOFFSETTO:+0200' . $this->newline
+ . 'TZNAME:CEST' . $this->newline
+ . 'DTSTART:19700329T020000' . $this->newline
+ . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3' . $this->newline
+ . 'END:DAYLIGHT' . $this->newline
+ . 'BEGIN:STANDARD' . $this->newline
+ . 'TZOFFSETFROM:+0200' . $this->newline
+ . 'TZOFFSETTO:+0100' . $this->newline
+ . 'TZNAME:CET' . $this->newline
+ . 'DTSTART:19701025T030000' . $this->newline
+ . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10' . $this->newline
+ . 'END:STANDARD' . $this->newline
+ . 'END:VTIMEZONE' . $this->newline;
+
+ return $header;
+ }
+
+ public function writeFooter()
+ {
+ return "END:VCALENDAR" . $this->newline;
+ }
+
+ /**
+ * Export this component as iCalendar format
+ *
+ * @param object $event The event to export.
+ * @return String iCalendar formatted data
+ */
+ public function write(Event &$event)
+ {
+
+ $match_pattern_1 = ['\\', '\n', ';', ','];
+ $replace_pattern_1 = ['\\\\', '\\n', '\;', '\,'];
+ $match_pattern_2 = ['\\', '\n', ';'];
+ $replace_pattern_2 = ['\\\\', '\\n', '\;'];
+ $exdate_time = 0;
+
+ $result = "BEGIN:VEVENT" . $this->newline;
+
+ foreach ($event->getProperties() as $name => $value) {
+ $params = [];
+ $params_str = '';
+
+ if ($name === 'SUMMARY') {
+ $value = $event->getTitle();
+ }
+ if ($value === '' || is_null($value)) {
+ continue;
+ }
+
+ switch ($name) {
+ // not supported event properties
+ case 'SEMNAME':
+ case 'EXPIRE':
+ case 'STUDIP_AUTHOR_ID':
+ case 'STUDIP_EDITOR_ID':
+ case 'STUDIP_ID':
+ case 'BEGIN':
+ case 'END':
+ case 'EVENT_TYPE':
+ case 'SEM_ID':
+ case 'STUDIP_GROUP_STATUS':
+ case 'STUDIP_CATEGORY':
+ continue 2;
+
+ // text fields
+ case 'SUMMARY':
+ $value = str_replace($match_pattern_1, $replace_pattern_1, $value);
+ break;
+ case 'DESCRIPTION':
+ $value = str_replace($match_pattern_1, $replace_pattern_1, $event->getDescription());
+ break;
+ case 'LOCATION':
+ $value = str_replace($match_pattern_1, $replace_pattern_1, $event->getLocation());
+ break;
+
+ case 'CATEGORIES':
+ $value = $this->_exportCategories($event);
+ break;
+
+ // Date fields
+ case 'LAST-MODIFIED':
+ case 'CREATED':
+ case 'COMPLETED':
+ $value = $this->_exportDateTime($value, true);
+ break;
+
+ case 'DTSTAMP':
+ $value = $this->_exportDateTime(time(), true);
+ break;
+
+ case 'DTSTART':
+ $exdate_time = $value;
+ case 'DTEND':
+ if ($event->isDayEvent()) {
+ $params['VALUE'] = 'DATE';
+ $params_str = ';VALUE=DATE';
+ $value++;
+ }
+ case 'DUE':
+ case 'RECURRENCE-ID':
+ if (array_key_exists('VALUE', $params)) {
+ if ($params['VALUE'] == 'DATE') {
+ $value = $this->_exportDate($value);
+ } else {
+ $value = $this->_exportDateTime($value);
+ $params_str = ';TZID=Europe/Berlin';
+ }
+ } else {
+ $value = $this->_exportDateTime($value);
+ $params_str = ';TZID=Europe/Berlin';
+ }
+ break;
+
+ case 'EXDATE':
+ if (array_key_exists('VALUE', $params)) {
+ $value = $this->_exportExDate($value, $params['VALUE']);
+ } else {
+ $value = $this->_exportExDateTime($value, $exdate_time);
+ }
+ $params_str = ';TZID=Europe/Berlin';
+ break;
+
+ case 'RDATE':
+ if (array_key_exists('VALUE', $params)) {
+ if ($params['VALUE'] == 'DATE') {
+ $value = $this->_exportDate($value);
+ } else if ($params['VALUE'] == 'PERIOD') {
+ $value = $this->_exportPeriod($value);
+ } else {
+ $value = $this->_exportDateTime($value);
+ }
+ } else {
+ $value = $this->_exportDateTime($value);
+ }
+ break;
+
+ case 'TRIGGER':
+ if (array_key_exists('VALUE', $params)) {
+ if ($params['VALUE'] == 'DATE-TIME') {
+ $value = $this->_exportDateTime($value);
+ } else if ($params['VALUE'] == 'DURATION') {
+ $value = $this->_exportDuration($value);
+ }
+ } else {
+ $value = $this->_exportDuration($value);
+ }
+ break;
+
+ // Duration fields
+ case 'DURATION':
+ $value = $this->_exportDuration($value);
+ break;
+
+ // Period of time fields
+ case 'FREEBUSY':
+ $value_str = '';
+ foreach ($value as $period) {
+ $value_str .= empty($value_str) ? '' : ',';
+ $value_str .= $this->_exportPeriod($period);
+ }
+ $value = $value_str;
+ break;
+
+
+ // UTC offset fields
+ case 'TZOFFSETFROM':
+ case 'TZOFFSETTO':
+ $value = $this->_exportUtcOffset($value);
+ break;
+
+ // Integer fields
+ case 'PERCENT-COMPLETE':
+ if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL)
+ $value = '';
+ case 'REPEAT':
+ case 'SEQUENCE':
+ $value = "$value";
+ break;
+
+ case 'PRIORITY':
+ if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL)
+ $value = '0';
+ else {
+ switch ($value) {
+ case 1:
+ $value = '1';
+ break;
+ case 2:
+ $value = '5';
+ break;
+ case 3:
+ $value = '9';
+ break;
+ default:
+ $value = '0';
+ }
+ }
+ break;
+
+ // Geo fields
+ case 'GEO':
+ if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL)
+ $value = '';
+ else
+ $value = $value['latitude'] . ',' . $value['longitude'];
+ break;
+
+ // Recursion fields
+ case 'EXRULE':
+ case 'RRULE':
+ if ($event->getRecurrence('rtype') != 'SINGLE')
+ $value = $this->_exportRecurrence($value);
+ else
+ continue 2;
+ break;
+
+ case "UID":
+ $value = "$value";
+ }
+ if ($name) {
+ $attr_string = "$name$params_str:$value";
+ $result .= $this->_foldLine($attr_string) . $this->newline;
+ }
+ }
+ // if ($event->isGroupEvent()) {
+ if ($event instanceof CalendarEvent && $event->attendees->count() > 1) {
+ $result .= $this->_exportGroupEventProperties($event);
+ }
+ // $result .= 'DTSTAMP:' . $this->_exportDateTime(time()) . $this->newline;
+ $result .= "END:VEVENT" . $this->newline;
+
+ return $result;
+ }
+
+ /**
+ * Export a UTC Offset field
+ *
+ * @param array $value
+ * @return String UTC offset field iCalendar formatted
+ */
+ public function _exportUtcOffset($value)
+ {
+ $offset = $value['ahead'] ? '+' : '-';
+ $offset .= sprintf('%02d%02d', $value['hour'], $value['minute']);
+ if (array_key_exists('second', $value)) {
+ $offset .= sprintf('%02d', $value['second']);
+ }
+
+ return $offset;
+ }
+
+ /**
+ * Export a Time Period field
+ *
+ * @param array $value
+ * @return String Period field iCalendar formatted
+ */
+ public function _exportPeriod($value)
+ {
+ $period = $this->_exportDateTime($value['start']);
+ $period .= '/';
+ if (array_key_exists('duration', $value)) {
+ $period .= $this->_exportDuration($value['duration']);
+ } else {
+ $period .= $this->_exportDateTime($value['end']);
+ }
+ return $period;
+ }
+
+ /**
+ * Export a DateTime field
+ *
+ * @param int $value Unix timestamp
+ * @return String Date and time (UTC) iCalendar formatted
+ */
+ public function _exportDateTime($value, $utc = false)
+ {
+
+// $TZOffset = 3600 * mb_substr(date('O', $value), 0, 3);
+// $TZOffset += 60 * mb_substr(date('O', $value), 3, 2);
+ //transform local time in UTC
+ if ($utc) {
+ $value -= date('Z', $value);
+ }
+
+ return $this->_exportDate($value) . 'T' . $this->_exportTime($value, $utc);
+ }
+
+ /**
+ * Export a Time field
+ *
+ * @param int $value Unix timestamp
+ * @return String Time (UTC) iCalendar formatted
+ */
+ public function _exportTime($value, $utc = false)
+ {
+ $time = date("His", $value);
+ if ($utc) {
+ $time .= 'Z';
+ }
+
+ return $time;
+ }
+
+ /**
+ * Export a Date field
+ */
+ public function _exportDate($value)
+ {
+ return date("Ymd", $value);
+ }
+
+ /**
+ * Export a duration value
+ */
+ public function _exportDuration($value)
+ {
+ $duration = '';
+ if ($value < 0) {
+ $value *= - 1;
+ $duration .= '-';
+ }
+ $duration .= 'P';
+
+ $weeks = floor($value / (7 * 86400));
+ $value = $value % (7 * 86400);
+ if ($weeks) {
+ $duration .= $weeks . 'W';
+ }
+
+ $days = floor($value / (86400));
+ $value = $value % (86400);
+ if ($days) {
+ $duration .= $days . 'D';
+ }
+
+ if ($value) {
+ $duration .= 'T';
+
+ $hours = floor($value / 3600);
+ $value = $value % 3600;
+ if ($hours) {
+ $duration .= $hours . 'H';
+ }
+
+ $mins = floor($value / 60);
+ $value = $value % 60;
+ if ($mins) {
+ $duration .= $mins . 'M';
+ }
+
+ if ($value) {
+ $duration .= $value . 'S';
+ }
+ }
+
+ return $duration;
+ }
+
+ /**
+ * Export a recurrence rule
+ */
+ public function _exportRecurrence($value)
+ {
+ $rrule = [];
+ // the last day of week in a MONTHLY or YEARLY recurrence in the
+ // Stud.IP calendar is 5, in iCalendar it is -1
+ if ($value['sinterval'] == '5')
+ $value['sinterval'] = '-1';
+
+ if ($value['count'])
+ unset($value['expire']);
+
+ foreach ($value as $r_param => $r_value) {
+ if ($r_value) {
+ switch ($r_param) {
+ case 'rtype':
+ $rrule[] = 'FREQ=' . $r_value;
+ break;
+ case 'expire':
+ // end of unix epoche (this is also the end of Stud.IP epoche ;-) )
+ if ($r_value < Calendar::CALENDAR_END)
+ $rrule[] = 'UNTIL=' . $this->_exportDateTime($r_value, true);
+ break;
+ case 'linterval':
+ $rrule[] = 'INTERVAL=' . $r_value;
+ break;
+ case 'wdays':
+ switch ($value['rtype']) {
+ case 'WEEKLY':
+ $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value);
+ break;
+ // Some CUAs (e.g. Outlook) don't understand the nWDAY syntax
+ // (where n is the nth ocurrence of the day in a given period of
+ // time and WDAY is the day of week) the RRULE uses the BYSETPOS
+ // rule.
+ case 'MONTHLY':
+ case 'YEARLY':
+ $rrule[] = 'BYDAY=' . $value['sinterval'] . $this->_exportWdays($r_value);
+ $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value);
+ // The Stud.IP calendar don't support multiple values in a
+ // comma separated list.
+
+ if ($value['sinterval'])
+ $rrule[] = 'BYSETPOS=' . $value['sinterval'];
+
+ break;
+ }
+ break;
+ case 'day':
+ $rrule[] = 'BYMONTHDAY=' . $r_value;
+ break;
+ case 'month':
+ $rrule[] = 'BYMONTH=' . $r_value;
+ break;
+ case 'count':
+ $rrule[] = 'COUNT=' . $r_value;
+ break;
+ }
+ }
+ }
+
+ if ($value['rtype'] == 'WEEKLY' && CALENDAR_WEEKSTART != 'MO') {
+ $rrule[] = 'WKST=' . CALENDAR_WEEKSTART;
+ }
+
+ return implode(';', $rrule);
+ }
+
+ /**
+ * Return the Stud.IP calendar wdays attribute of a event recurrence
+ */
+ public function _exportWdays($value)
+ {
+ $wdays_map = ['1' => 'MO', '2' => 'TU', '3' => 'WE', '4' => 'TH', '5' => 'FR',
+ '6' => 'SA', '7' => 'SU'];
+ $wdays = [];
+ preg_match_all('/(\d)/', $value, $matches);
+ foreach ($matches[1] as $match) {
+ $wdays[] = $wdays_map[$match];
+ }
+
+ return implode(',', $wdays);
+ }
+
+ public function _exportExDate($value, $param)
+ {
+ $exdates = [];
+ $date_times = explode(',', $value);
+ foreach ($date_times as $date_time) {
+ $exdates[] = $this->_exportDate($date_time);
+ }
+
+ return implode(',', $exdates);
+ }
+
+ public function _exportExDateTime($value, $param)
+ {
+ $exdates = [];
+ $date_times = explode(',', $value);
+ foreach ($date_times as $date_time) {
+ $exdates[] = $this->_exportDate($date_time) . 'T' . $this->_exportTime($param);
+ }
+
+ return implode(',', $exdates);
+ }
+
+ private function _exportGroupEventProperties(Event $event)
+ {
+ $organizer = User::find($event->getAuthorId());
+ if ($organizer) {
+ $properties = $this->_foldLine('ORGANIZER;CN="'
+ . $organizer->getFullName()
+ . '":mailto:' . $organizer->Email)
+ . $this->newline;
+ } else {
+ $properties = $this->_foldLine('ORGANIZER;CN="'
+ . _('unbekannt')
+ . '":mailto:' . $GLOBALS['user']->email)
+ . $this->newline;
+ }
+ foreach ($event->attendees as $event_member) {
+ if ($event->getAuthorId() == $event_member->user->id) {
+ if ($event_member->user) {
+ $properties .= $this->_foldLine('ATTENDEE;'
+ . 'ROLE=REQ-PARTICIPANT;'
+ . 'CN="' . $event_member->user->getFullName()
+ . '":mailto:' . $event_member->user->Email)
+ . $this->newline;
+ } else {
+ $properties = '';
+ /*
+ $properties .= $this->_foldLine('ATTENDEE;'
+ . 'ROLE=REQ-PARTICIPANT;'
+ . 'CN="' . _('unbekannt') . '"')
+ . $this->newline;
+ *
+ */
+ }
+ } else {
+ if ($event_member->user) {
+ switch ($event_member->group_status) {
+ case CalendarEvent::PARTSTAT_ACCEPTED :
+ $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT'
+ . ';PARTSTAT=ACCEPTED';
+ break;
+ case CalendarEvent::PARTSTAT_DELEGATED :
+ $attendee = 'ATTENDEE;ROLE=NON-PARTICIPANT'
+ . ';PARTSTAT=ACCEPTED'
+ . ';DELEGATED-TO="mailto:'
+ . $this->getFacultyEmail($organizer->getId())
+ . '"';
+ break;
+ case CalendarEvent::PARTSTAT_DECLINED :
+ $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT'
+ . ';PARTSTAT=DECLINED';
+ break;
+ default :
+ $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT';
+ $attendee .= ';PARTSTAT=TENTATIVE';
+ $attendee .= ';RSVP=TRUE';
+
+ }
+ $attendee .= ';CN="' . $event_member->user->getFullName()
+ . '":mailto:' . $event_member->user->Email;
+ /*
+ } else {
+ $attendee .= ';CN="' . _('unbekannt') . '"';
+ }
+ *
+ */
+ $properties .= $this->_foldLine($attendee) . $this->newline;
+ }
+ }
+ }
+ return $properties;
+ }
+
+ public function getFacultyEmail($user_id)
+ {
+ $stmt = DBManager::get()->prepare('SELECT email FROM Institute i '
+ . 'LEFT JOIN user_inst ui USING(institut_id) '
+ . 'WHERE i.Institut_id = fakultaets_id AND user_id = ?');
+ $stmt->execute([$user_id]);
+ return $stmt->fetchColumn();
+ }
+
+ public function _exportCategories($event)
+ {
+ return implode(',' ,$event->toStringCategories(true));
+ }
+
+ /**
+ * Return the folded version of a line
+ */
+ public function _foldLine($line)
+ {
+ $line = preg_replace('/(\r\n|\n|\r)/', '\n', $line);
+ if (mb_strlen($line) > 75) {
+ $foldedline = '';
+ while ($line !== '') {
+ $maxLine = mb_substr($line, 0, 75);
+ $cutPoint = max(60, max(mb_strrpos($maxLine, ';'), mb_strrpos($maxLine, ':')) + 1);
+
+ $foldedline .= ( empty($foldedline)) ?
+ mb_substr($line, 0, $cutPoint) :
+ $this->newline . ' ' . mb_substr($line, 0, $cutPoint);
+
+ $line = (mb_strlen($line) <= $cutPoint) ? '' : mb_substr($line, $cutPoint);
+ }
+ return $foldedline;
+ }
+ return $line;
+ }
+}
diff --git a/lib/calendar/EventData.class.php b/lib/calendar/EventData.class.php
new file mode 100644
index 0000000..9ab4377
--- /dev/null
+++ b/lib/calendar/EventData.class.php
@@ -0,0 +1,92 @@
+<?php
+
+
+namespace Studip\Calendar;
+
+
+class EventData
+{
+ public $begin;
+ public $end;
+ public $title;
+ public $event_classes;
+ public $text_colour;
+ public $background_colour;
+ public $editable;
+ public $object_class;
+ public $object_id;
+ public $parent_object_class;
+ public $parent_object_id;
+ public $range_type;
+ public $range_id;
+ public $view_urls;
+ public $api_urls;
+ public $icon;
+
+ public function __construct(
+ \DateTime $begin,
+ \DateTime $end,
+ string $title,
+ Array $event_classes,
+ string $text_colour,
+ string $background_colour,
+ bool $editable,
+ string $object_class,
+ string $object_id,
+ string $parent_object_class,
+ string $parent_object_id,
+ string $range_type,
+ string $range_id,
+ Array $view_urls = [],
+ Array $api_urls = [],
+ string $icon = ''
+ )
+ {
+ $this->begin = $begin;
+ $this->end = $end;
+ $this->title = $title;
+ $this->event_classes = $event_classes;
+ $this->text_colour = $text_colour;
+ $this->background_colour = $background_colour;
+ $this->editable = $editable;
+ $this->object_class = $object_class;
+ $this->object_id = $object_id;
+ $this->parent_object_class = $parent_object_class;
+ $this->parent_object_id = $parent_object_id;
+ $this->range_type = $range_type;
+ $this->range_id = $range_id;
+ $this->view_urls = $view_urls;
+ $this->api_urls = $api_urls;
+ $this->icon = $icon;
+ }
+
+
+ public function toFullcalendarEvent()
+ {
+ //Note: The timezone must not be transmitted or
+ //the events may be shifted when there is a timezone
+ //or daylight saving time difference between the server
+ //and the client!
+ return [
+ 'resourceId' => $this->range_id,
+ 'start' => $this->begin->format('Y-m-d\TH:i:s'),
+ 'end' => $this->end->format('Y-m-d\TH:i:s'),
+ 'title' => $this->title,
+ 'classNames' => $this->event_classes,
+ 'textColor' => $this->text_colour,
+ 'color' => $this->background_colour,
+ 'editable' => $this->editable,
+ 'studip_weekday_begin' => $this->begin->format('N'),
+ 'studip_weekday_end' => $this->end->format('N'),
+ 'studip_object_class' => $this->object_class,
+ 'studip_object_id' => $this->object_id,
+ 'studip_parent_object_class' => $this->parent_object_class,
+ 'studip_parent_object_id' => $this->parent_object_id,
+ 'studip_range_type' => $this->range_type,
+ 'studip_range_id' => $this->range_id,
+ 'studip_view_urls' => $this->view_urls,
+ 'studip_api_urls' => $this->api_urls,
+ 'icon' => $this->icon
+ ];
+ }
+}
diff --git a/lib/calendar/EventSource.interface.php b/lib/calendar/EventSource.interface.php
new file mode 100644
index 0000000..48506d7
--- /dev/null
+++ b/lib/calendar/EventSource.interface.php
@@ -0,0 +1,64 @@
+<?php
+
+
+namespace Studip\Calendar;
+
+
+/**
+ * This interface defines methods that can be implemented by classes
+ * that can provide calendar event data in a standardised format.
+ * The methods are meant to be called on a "per-object base",
+ * meaning that filtering of the event sources has to be done
+ * before calling methods of this interface.
+ */
+interface EventSource
+{
+ /**
+ * Returns all event data this event source can provide.
+ *
+ * @returns EventData[] An array of Studip\Calendar\EventData objects.
+ */
+ public function getAllEventData();
+
+
+ /**
+ * Returns event data that fall in the specified time range.
+ *
+ * @returns EventData[] An array of Studip\Calendar\EventData objects.
+ */
+ public function getEventDataForTimeRange(\DateTime $begin, \DateTime $end);
+
+
+ /**
+ * Allows a filtered output of event data based on a specified user,
+ * a specified range-ID and range type (must be supported by the
+ * Context class) or a specified time range.
+ * If no filters are applied the result should be the same as if
+ * the getAllEventData method from this interface is called.
+ *
+ * @param string $user_id The user for whom the event data shall be
+ * retrieved. Depending on the implementation, this parameter
+ * might be necessary to check permissions for event objects.
+ *
+ * @param string $range_id An optional range-ID that may be necessary
+ * for an implementation to check for permissions.
+ *
+ * @param string $range_type An optional range type that may be
+ * necessary for an implementation to check for permissions.
+ *
+ * @param DateTime|int|string $begin The begin date as DateTime object
+ * or unix timestamp.
+ *
+ * @param DateTime|int|string $end The end date as DateTime object
+ * or unix timestamp.
+ *
+ * @returns EventData[] An array of Studip\Calendar\EventData objects.
+ */
+ public function getFilteredEventData(
+ $user_id = null,
+ $range_id = null,
+ $range_type = null,
+ $begin = null,
+ $end = null
+ );
+}
diff --git a/lib/calendar/lib/CalendarError.class.php b/lib/calendar/lib/CalendarError.class.php
new file mode 100644
index 0000000..f796e1a
--- /dev/null
+++ b/lib/calendar/lib/CalendarError.class.php
@@ -0,0 +1,56 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * Error.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+
+class CalendarError
+{
+
+ var $status;
+ var $message;
+ var $file;
+ var $line;
+
+ public function __construct($status, $message, $file = '', $line = '')
+ {
+ $this->status = $status;
+ $this->message = $message;
+ $this->file = $file;
+ $this->line = $line;
+ }
+
+ public function getStatus()
+ {
+ return $this->status;
+ }
+
+ public function getMessage()
+ {
+ return $this->message;
+ }
+
+ public function getFile()
+ {
+ return $this->file;
+ }
+
+ public function getLine()
+ {
+ return $this->line;
+ }
+
+}
diff --git a/lib/calendar/lib/ErrorHandler.class.php b/lib/calendar/lib/ErrorHandler.class.php
new file mode 100644
index 0000000..710bdd1
--- /dev/null
+++ b/lib/calendar/lib/ErrorHandler.class.php
@@ -0,0 +1,121 @@
+<?
+# Lifter002: TODO
+# Lifter007: TODO
+
+/**
+ * ErrorHandler.class.php
+ *
+ * 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.
+ *
+ * @author Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package calendar
+ */
+
+class ErrorHandler
+{
+ // this is the state of the error handling if no error has occured
+ const ERROR_NORMAL = 1;
+ // this is the state of the error handling a message has to be displayed
+ const ERROR_MESSAGE = 2;
+ // this is the state of the error handling if something was going wrong but without data-loss
+ const ERROR_WARNING = 4;
+ // this is the state of the error handling if a critical error occured (maybe with data loss)
+ const ERROR_CRITICAL = 8;
+ // this is the state of the error handling if a fatal error occured and the execution of the process (e.g. import of events) was stopped
+ const ERROR_FATAL = 16;
+
+ var $errors;
+ var $status;
+
+ public function __construct()
+ {
+
+ $this->errors = [];
+ $this->status = ErrorHandler::ERROR_NORMAL;
+ $this->_is_instantiated = true;
+ }
+
+ public function getStatus($status = NULL)
+ {
+ if ($status === NULL)
+ return $this->status;
+
+ return $status & $this->status;
+ }
+
+ public function getMaxStatus($status)
+ {
+ if ($status <= $this->status)
+ return true;
+
+ return false;
+ }
+
+ public function getMinStatus($status)
+ {
+ if ($status >= $this->status)
+ return true;
+
+ return false;
+ }
+
+ public function getErrors($status = NULL)
+ {
+ if ($status === NULL)
+ return $this->errors;
+
+ return $this->errors[$status];
+ }
+
+ public function getAllErrors()
+ {
+ $status = [ErrorHandler::ERROR_FATAL, ErrorHandler::ERROR_CRITICAL, ErrorHandler::ERROR_WARNING,
+ ErrorHandler::ERROR_MESSAGE, ErrorHandler::ERROR_NORMAL];
+ $errors = [];
+ foreach ($status as $stat) {
+ if (is_array($this->errors[$stat])) {
+ $errors = array_merge($errors, $this->errors[$stat]);
+ }
+ }
+ return $errors;
+ }
+
+ public function nextError($status)
+ {
+ if(is_array($this->errors[$status])) {
+ return reset($this->errors[$status]);
+ }
+ return false;
+ }
+
+ public function throwError($status, $message, $file = '', $line = '')
+ {
+ $this->errors[$status][] = new CalendarError($status, $message, $file, $line);
+ $this->status |= $status;
+ reset($this->errors[$status]);
+ if ($status == ErrorHandler::ERROR_FATAL) {
+ echo '<b>';
+ while ($error = $this->nextError(ErrorHandler::ERROR_FATAL)) {
+ echo '<br />' . $error->getMessage();
+ }
+ echo '</b><br />';
+ page_close();
+ exit;
+ }
+ }
+
+ public function throwSingleError($index, $status, $message, $file = '', $line = '')
+ {
+ static $index_list = [];
+
+ if ($index_list[$index] != 1) {
+ $this->throwError($status, $message, $file, $line);
+ $index_list[$index] = 1;
+ }
+ }
+}