aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Hackl <hackl@data-quest.de>2024-11-25 08:41:07 +0000
committerThomas Hackl <hackl@data-quest.de>2024-11-25 08:41:07 +0000
commitd1375e5f7b5d7543ec694df7c2f47b0a967f8951 (patch)
tree68f494e37de796f46e13d79caafb8f8b7cb2072c
parenta69083a3ec3fcd8fb510fc6ea4c53f6ffcb3e436 (diff)
Resolve "Garuda in den Kern übernehmen"
Closes #3326 Merge request studip/studip!3035
-rw-r--r--app/controllers/admin/courses.php23
-rw-r--r--app/controllers/massmail/message.php394
-rw-r--r--app/controllers/massmail/overview.php32
-rw-r--r--app/controllers/massmail/permissions.php174
-rw-r--r--app/controllers/massmail/quick.php90
-rw-r--r--app/controllers/massmail/settings.php109
-rw-r--r--app/views/admin/courses/massmail.php10
-rw-r--r--db/migrations/6.0.32_integrate_garuda_plugin.php285
-rw-r--r--lib/admissionrules/conditionaladmission/ConditionalAdmission.php2
-rw-r--r--lib/admissionrules/preferentialadmission/PreferentialAdmission.php1
-rw-r--r--lib/classes/JsonApi/RouteMap.php9
-rw-r--r--lib/classes/JsonApi/Routes/MassMail/Authority.php30
-rw-r--r--lib/classes/JsonApi/Routes/MassMail/MassMailMessagesIndex.php71
-rw-r--r--lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsIndex.php31
-rw-r--r--lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsShow.php32
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/Authority.php11
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php38
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php8
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php12
-rw-r--r--lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php14
-rw-r--r--lib/classes/JsonApi/SchemaMap.php6
-rw-r--r--lib/classes/JsonApi/Schemas/Degree.php78
-rw-r--r--lib/classes/JsonApi/Schemas/MassMailMessage.php84
-rw-r--r--lib/classes/JsonApi/Schemas/MassMailPermission.php121
-rw-r--r--lib/classes/UserFilter.php (renamed from lib/classes/admission/UserFilter.php)110
-rw-r--r--lib/classes/UserFilterField.php (renamed from lib/classes/admission/UserFilterField.php)105
-rw-r--r--lib/classes/UserFilterFields/DatafieldCondition.php (renamed from lib/classes/admission/userfilter/DatafieldCondition.php)32
-rw-r--r--lib/classes/UserFilterFields/DegreeCondition.php (renamed from lib/classes/admission/userfilter/DegreeCondition.php)7
-rw-r--r--lib/classes/UserFilterFields/DomainCondition.php45
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailDegreeFilter.php126
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailDomainFilter.php75
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailGenderFilter.php74
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailInstituteFilter.php140
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailPermissionFilter.php111
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailSelfAssignedInstituteFilter.php137
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailSemesterOfStudyFilter.php75
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailStatusgroupFilter.php88
-rw-r--r--lib/classes/UserFilterFields/MassMail/MassMailSubjectFilter.php125
-rw-r--r--lib/classes/UserFilterFields/PermissionCondition.php (renamed from lib/classes/admission/userfilter/PermissionCondition.php)7
-rw-r--r--lib/classes/UserFilterFields/SemesterOfStudyCondition.php (renamed from lib/classes/admission/userfilter/SemesterOfStudyCondition.php)11
-rw-r--r--lib/classes/UserFilterFields/StgteilVersionCondition.php (renamed from lib/classes/admission/userfilter/StgteilVersionCondition.php)17
-rw-r--r--lib/classes/UserFilterFields/SubjectCondition.php (renamed from lib/classes/admission/userfilter/SubjectCondition.php)7
-rw-r--r--lib/classes/UserFilterFields/SubjectConditionAny.php (renamed from lib/classes/admission/userfilter/SubjectConditionAny.php)6
-rw-r--r--lib/classes/UserFilterRange.php29
-rw-r--r--lib/classes/admission/CourseSet.php46
-rw-r--r--lib/classes/forms/CheckboxCollectionInput.php25
-rw-r--r--lib/classes/forms/Fieldset.php17
-rw-r--r--lib/classes/forms/FileInput.php23
-rw-r--r--lib/classes/forms/Form.php20
-rw-r--r--lib/classes/forms/QuicksearchListInput.php19
-rw-r--r--lib/classes/forms/SerialWysiwygInput.php34
-rw-r--r--lib/classes/forms/UserFilterInput.php60
-rw-r--r--lib/cronjobs/send_massmails.php107
-rw-r--r--lib/models/MassMail/MassMailFilter.php34
-rw-r--r--lib/models/MassMail/MassMailMarker.php181
-rw-r--r--lib/models/MassMail/MassMailMessage.php373
-rw-r--r--lib/models/MassMail/MassMailPermission.php139
-rw-r--r--lib/models/MassMail/MassMailToken.php25
-rw-r--r--lib/navigation/MessagingNavigation.php31
-rw-r--r--resources/vue/base-components.js4
-rw-r--r--resources/vue/components/StudipUserFilter.vue20
-rw-r--r--resources/vue/components/StudipWysiwyg.vue2
-rw-r--r--resources/vue/components/form_inputs/FileUpload.vue198
-rw-r--r--resources/vue/components/form_inputs/QuicksearchListInput.vue97
-rw-r--r--resources/vue/components/form_inputs/SerialTextMarkers.vue80
-rw-r--r--resources/vue/components/form_inputs/UserFilterInput.vue146
-rw-r--r--resources/vue/components/massmail/MassMailMessagesList.vue153
-rw-r--r--resources/vue/components/massmail/MassMailPermissions.vue108
-rw-r--r--templates/forms/checkbox_collection_input.php33
-rw-r--r--templates/forms/fieldset.php10
-rw-r--r--templates/forms/file_input.php11
-rw-r--r--templates/forms/form.php1
-rw-r--r--templates/forms/quicksearchlist_input.php18
-rw-r--r--templates/forms/radio_input.php16
-rw-r--r--templates/forms/serial_wysiwyg_input.php17
-rw-r--r--templates/forms/textarea_input.php28
-rw-r--r--templates/forms/user_filter_input.php19
77 files changed, 4928 insertions, 159 deletions
diff --git a/app/controllers/admin/courses.php b/app/controllers/admin/courses.php
index 3d63cd9..890a9ab 100644
--- a/app/controllers/admin/courses.php
+++ b/app/controllers/admin/courses.php
@@ -487,6 +487,16 @@ class Admin_CoursesController extends AuthenticatedController
'data-dialog' => 'size=big'
]);
break;
+ case 23: // Mass mail to selected courses
+ $data['buttons_top'] = '<label>' . _('Alle auswählen') .
+ '<input type="checkbox" data-proxyfor=".course-admin td:last-child :checkbox"></label>';
+ $data['buttons_bottom'] = (string) \Studip\Button::createAccept(
+ _('Nachricht an ausgewählte Veranstaltungen'), 'massmail',
+ [
+ 'formaction' => URLHelper::getURL('dispatch.php/massmail/quick/courses'),
+ 'data-dialog' => 'size=big'
+ ]);
+ break;
default:
foreach (PluginManager::getInstance()->getPlugins(AdminCourseAction::class) as $plugin) {
if ($GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA === get_class($plugin)) {
@@ -837,6 +847,11 @@ class Admin_CoursesController extends AuthenticatedController
$template->course = $course;
$d['action'] = $template->render();
break;
+ case 23: //Masssenexport Teilnehmendendaten
+ $template = $tf->open('admin/courses/massmail');
+ $template->course = $course;
+ $d['action'] = $template->render();
+ break;
default:
foreach (PluginManager::getInstance()->getPlugins(AdminCourseAction::class) as $plugin) {
if ($GLOBALS['user']->cfg->MY_COURSES_ACTION_AREA === get_class($plugin)) {
@@ -1435,6 +1450,14 @@ class Admin_CoursesController extends AuthenticatedController
'partial' => 'batch_export_members.php'
],
+ 23 => [
+ 'name' => _('Nachricht schreiben'),
+ 'title' => _('Nachricht schreiben'),
+ 'url' => 'dispatch.php/massmail/quick/courses',
+ 'dialogform' => true,
+ 'multimode' => true,
+ 'partial' => 'massmail.php'
+ ]
];
if (!$GLOBALS['perm']->have_perm('admin')) {
diff --git a/app/controllers/massmail/message.php b/app/controllers/massmail/message.php
new file mode 100644
index 0000000..3f7a009
--- /dev/null
+++ b/app/controllers/massmail/message.php
@@ -0,0 +1,394 @@
+<?php
+
+class Massmail_MessageController extends \AuthenticatedController
+{
+
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ if (!\MassMail\MassMailPermission::has(User::findCurrent()->id)) {
+ throw new AccessDeniedException();
+ }
+ }
+
+ public function index_action($id = null)
+ {
+ Navigation::activateItem('/messaging/massmail/message');
+ PageLayout::setTitle(_('Nachricht an Zielgruppe schreiben'));
+
+ $message = new \MassMail\MassMailMessage($id);
+
+ $temp_id = $id ?: uniqid(md5(time()));
+ $folder = $message->findFolder($temp_id);
+
+ // SearchType needed for course selection
+ $courseSearch = new StandardSearch('Seminar_id');
+
+ // SearchType needed for user
+ $userSearch = new StandardSearch('user_id');
+
+ $form = \Studip\Forms\Form::fromSORM(
+ $message,
+ [
+ 'legend' => _('Grunddaten'),
+ 'collapsed' => false,
+ 'collapsable' => false,
+ 'fields' => [
+ 'target' => [
+ 'type' => 'select',
+ 'required' => true,
+ 'label' => _('Zielgruppe'),
+ 'value' => $message->target ?? 'all',
+ 'options' => \MassMail\MassMailMessage::getTargets()
+ ],
+ 'student_filters' => [
+ 'type' => 'userFilter',
+ 'label' => _('Auswahlfilter'),
+ 'if' => 'target === "students"',
+ 'context' => 'MassMail',
+ 'target' => 'students',
+ ':key' => 'NaN',
+ 'store' => function($value, $input) {
+ $filters = [];
+ foreach ($value as $one) {
+ $filter = new UserFilter($one['id'] ?? '');
+ $filter->fields = [];
+ foreach ($one['attributes']['fields'] as $field) {
+ $classname = $field['attributes']['type'];
+ $f = new $classname();
+ if (!empty($fiele['id'])) {
+ $f->setId($field['id']);
+ }
+ $f->setCompareOperator($field['attributes']['compare-operator']);
+ $f->setValue($field['attributes']['value']);
+ $filter->addField($f);
+ }
+ $filter->store();
+ $connection = new \MassMail\MassMailFilter();
+ $connection->filter_id = $filter->getId();
+ $filters[] = $connection;
+ }
+ $input->getContextObject()->filters = $filters;
+ }
+ ],
+ 'employee_filters' => [
+ 'type' => 'userFilter',
+ 'label' => _('Auswahlfilter'),
+ 'if' => 'target === "employees"',
+ 'context' => 'MassMail',
+ 'target' => 'employees',
+ ':key' => 'NaN',
+ 'store' => function($value, $input) {
+ $filters = [];
+ foreach ($value as $one) {
+ $filter = new UserFilter($one['id'] ?? '');
+ $filter->fields = [];
+ foreach ($one['attributes']['fields'] as $field) {
+ $classname = $field['attributes']['type'];
+ $f = new $classname();
+ if (!empty($fiele['id'])) {
+ $f->setId($field['id']);
+ }
+ $f->setCompareOperator($field['attributes']['compare-operator']);
+ $f->setValue($field['attributes']['value']);
+ $filter->addField($f);
+ }
+ $filter->store();
+ $connection = new \MassMail\MassMailFilter();
+ $connection->filter_id = $filter->getId();
+ $filters[] = $connection;
+ }
+ $input->getContextObject()->filters = $filters;
+ }
+ ],
+ 'semester' => [
+ 'type' => 'select',
+ 'label' => _('Semester wählen'),
+ 'value' => $message->config['semester'] ?? \Semester::findDefault()->id,
+ 'if' => 'target === "lecturers"',
+ 'options' => \MassMail\MassMailMessage::getSemesters(),
+ 'store' => function($value, $input) {
+ if ($input->getContextObject()->target === 'lecturers') {
+ $input->getContextObject()->config = ['semester' => $value];
+ }
+ }
+ ],
+ 'courses' => [
+ 'type' => 'quicksearchList',
+ 'label' => _('Veranstaltungen wählen'),
+ 'value' => json_encode($message->config?->getArrayCopy()['courses'] ?? []),
+ 'if' => 'target === "courses"',
+ 'searchtype' => $courseSearch,
+ 'store' => function($value, $input) {
+ if ($input->getContextObject()->target === 'courses') {
+ $input->getContextObject()->config = [];
+ $input->getContextObject()->config['courses'] = \Course::findAndMapMany(
+ function ($course) {
+ return ['id' => $course->id, 'name' => $course->getFullname()];
+ },
+ json_decode($value, true)
+ );
+ }
+ }
+ ],
+ 'course_perm' => [
+ 'type' => 'select',
+ 'label' => _('Berechtigungsebene wählen'),
+ 'value' => $message->config['perm'] ?? 'autor',
+ 'if' => 'target === "courses"',
+ 'options' => [
+ 'dozent' => get_title_for_status('dozent', 2, 1),
+ 'tutor' => get_title_for_status('tutor', 2, 1),
+ 'autor' => get_title_for_status('autor', 2, 1),
+ 'user' => get_title_for_status('user', 2, 1),
+ ],
+ 'store' => function($value, $input) {
+ if ($input->getContextObject()->target === 'courses') {
+ $input->getContextObject()->config['perm'] = $value;
+ }
+ }
+ ],
+ 'manual_usernames' => [
+ 'type' => 'textarea',
+ 'label' => _('Liste von Benutzernamen, durch Zeilenumbruch getrennt'),
+ 'if' => 'target === "usernames"',
+ 'value' => $message->config['usernames'] ?? '',
+ 'store' => function($value, $input) {
+ if ($input->getContextObject()->target === 'usernames') {
+ $input->getContextObject()->config = [];
+ $input->getContextObject()->config['usernames'] = $value;
+ }
+ }
+ ],
+ 'subject' => [
+ 'type' => 'text',
+ 'required' => true,
+ 'label' => _('Betreff'),
+ 'value' => $message->subject
+ ],
+ 'message' => [
+ 'type' => 'serialWysiwyg',
+ 'required' => true,
+ 'label' => _('Nachricht'),
+ 'value' => $message->message,
+ 'markers' => json_encode(
+ array_map(
+ fn ($m) => $m->toArray(),
+ \MassMail\MassMailMarker::findAll(
+ \MassMail\MassMailPermission::has(User::findCurrent()->id, true)
+ )
+ )
+ )
+ ]
+ ]
+ ],
+ $this->url_for('massmail/overview')
+ )->addSORM($message, [
+ 'legend' => _('Weitere Einstellungen'),
+ 'collapsable' => true,
+ 'collapsed' => true,
+ 'fields' => [
+ 'author_id' => [
+ 'type' => 'hidden',
+ 'value' => User::findCurrent()->id
+ ],
+ 'attachments' => [
+ 'type' => 'file',
+ 'label' => _('Dateianhänge auswählen'),
+ 'value' => $message->folder_id ?? $message->folder_id = $folder->id,
+ 'upload_url' => $this->url_for('massmail/message/attachments', $folder->id),
+ 'multiple' => true,
+ 'if' => $GLOBALS['ENABLE_EMAIL_ATTACHMENTS']
+ ? 'true' : 'false',
+ 'store' => function($value, $input) {
+ $input->getContextObject()->folder_id = $value;
+ }
+ ],
+ 'tokens' => [
+ 'type' => 'file',
+ 'label' => _('CSV mit Teilnahmecodes auswählen'),
+ 'value' => $message->folder_id ?? $message->folder_id = $folder->id,
+ 'upload_url' => $this->url_for('massmail/message/tokens', $message->folder_id),
+ 'accept' => '.csv,.txt',
+ 'if' => \MassMail\MassMailPermission::has(User::findCurrent()->id, true)
+ ? 'true' : 'false',
+ 'store' => function($value, $input) {
+ $input->getContextObject()->folder_id = $value;
+ }
+ ],
+ 'send_at_date' => [
+ 'type' => 'datetimepicker',
+ 'label' => _('Zu einem späteren Zeitpunkt senden'),
+ 'value' => $message->send_at_date ?? time()
+ ],
+ 'send_as' => [
+ 'type' => 'select',
+ 'label' => ('Nachricht senden als'),
+ 'value' => $message->sender_id ?? User::findCurrent()->id,
+ 'if' => \MassMail\MassMailPermission::has(User::findCurrent()->id, true)
+ ? 'true' : 'false',
+ 'options' => [
+ User::findCurrent()->id => _('Von meiner Kennung verschicken'),
+ 'user_id' => _('Eine andere Person eintragen'),
+ '____%system%____' => _('Anonym, mit "Stud.IP" als Absender')
+ ],
+ 'store' => function($value, $input) {
+ if ($value === User::findCurrent()->id || $value === '____%system%____') {
+ $input->getContextObject()->sender_id = $value;
+ }
+ }
+ ],
+ 'sender_id' => [
+ 'type' => 'quicksearch',
+ 'label' => _('Absender:in wählen'),
+ 'value' => $message->sender_id ?? '',
+ 'if' => 'send_as === "user_id"',
+ 'searchtype' => $userSearch,
+ 'store' => function($value, $input) {
+ $sender_id = $input->getContextObject()->sender_id;
+ if ($sender_id !== User::findCurrent()->id && $sender_id !== '____%system%____') {
+ $input->sender_id = $value;
+ }
+ }
+ ],
+ 'exclude_users' => [
+ 'type' => 'textarea',
+ 'label' => _('Liste von Benutzernamen, die die Nachricht nicht erhalten sollen'),
+ 'value' => $message->exclude_users ?? ''
+ ],
+ 'cc' => [
+ 'type' => 'textarea',
+ 'label' => _('Liste von Benutzernamen, die die Nachricht als Kopie erhalten sollen'),
+ 'value' => $message->cc ?? ''
+ ],
+ 'flags' => [
+ 'type' => 'radio',
+ 'label' => _('Besondere Kennzeichnung'),
+ 'value' => $message->is_template
+ ? 'is_template'
+ : ($message->protected ? 'protected' : ''),
+ 'options' => [
+ '' => _('Keine besondere Kennzeichnung'),
+ 'is_template' => _('Nicht verschicken, sondern als Vorlage speichern'),
+ 'protected' => _('Auch nach dem Versand dauerhaft speichern')
+ ],
+ 'store' => function($value, $input) {
+ switch ($value) {
+ case 'is_template':
+ $input->getContextObject()->is_template = 1;
+ $input->getContextObject()->protected = 0;
+ break;
+ case 'protected':
+ $input->getContextObject()->is_template = 0;
+ $input->getContextObject()->protected = 1;
+ break;
+ default:
+ $input->getContextObject()->is_template = 0;
+ $input->getContextObject()->protected = 0;
+ break;
+ }
+ }
+ ]
+ ]
+ ])->addStoreCallback(function ($form) {
+ $message = $form->getLastPart()->getContextObject();
+
+ // Adjust folder range_id to the actual message id.
+ $folder = Folder::find($message->folder_id);
+ $folder->range_id = $message->id;
+ $folder->store();
+
+ // Create message tokens if necessary.
+ if ($message->hasMarkers('token')) {
+ foreach ($folder->getTypedFolder()->getFiles() as $ref) {
+ if (isset($ref->file->metadata['is_token_file'])) {
+ $file = fopen($ref->file->getPath(), 'r');
+ while (!feof($file)) {
+ $token = fgets($file);
+ $t = new \MassMail\MassMailToken();
+ $t->message_id = $message->id;
+ $t->token = $token;
+ $t->store();
+ }
+ }
+ }
+ }
+ })->autoStore();
+
+ $this->render_form($form);
+ }
+
+ public function delete_action(int $id)
+ {
+ $message = \MassMail\MassMailMessage::find($id);
+
+ if (
+ !$message
+ || (
+ $message->author_id !== User::findCurrent()->id
+ && !\MassMail\MassMailPermission::has(User::findCurrent()->id, true)
+ )
+ ) {
+ throw new AccessDeniedException();
+ }
+
+ if ($message->delete() !== false) {
+ PageLayout::postSuccess(_('Die Nachricht wurde gelöscht.'));
+ } else {
+ PageLayout::postError(_('Die Nachricht konnte nicht gelöscht werden.'));
+ }
+
+ $this->relocate('massmail/overview');
+ }
+
+ public function attachments_action(string $folder_id)
+ {
+ if (!$GLOBALS['ENABLE_EMAIL_ATTACHMENTS']) {
+ throw new AccessDeniedException();
+ }
+
+ $folder = Folder::find($folder_id)->getTypedFolder();
+ $uploaded = FileManager::handleFileUpload($_FILES['attachments'], $folder);
+
+ if (!empty($uploaded['error'])) {
+ $this->set_status(400);
+ $this->render_text(implode('<br>' . $uploaded['error']));
+ } else {
+ $this->render_nothing();
+ }
+ }
+
+ public function tokens_action(string $folder_id)
+ {
+ if (!\MassMail\MassMailPermission::has(User::findCurrent()->id, true)) {
+ throw new AccessDeniedException();
+ }
+
+ $data = [
+ 'name' => [$_FILES['tokens']['name']],
+ 'tmp_name' => [$_FILES['tokens']['tmp_name']],
+ 'type' => [$_FILES['tokens']['type']],
+ 'error' => [$_FILES['tokens']['error']],
+ 'size' => [$_FILES['tokens']['size']],
+ ];
+
+ $folder = Folder::find($folder_id)->getTypedFolder();
+ $uploaded = FileManager::handleFileUpload($data, $folder);
+
+ if (!empty($uploaded['error'])) {
+ $this->set_status(400);
+ $this->render_text(implode('<br>' . $uploaded['error']));
+ } else {
+
+ // Set metadata for created file, indicating that this is a file with message tokens.
+ foreach ($uploaded['files'] as $ref) {
+ $ref->file->metadata = ['is_token_file' => true];
+ $ref->file->store();
+ }
+
+ $this->render_nothing();
+ }
+ }
+
+}
diff --git a/app/controllers/massmail/overview.php b/app/controllers/massmail/overview.php
new file mode 100644
index 0000000..db71d35
--- /dev/null
+++ b/app/controllers/massmail/overview.php
@@ -0,0 +1,32 @@
+<?php
+
+class Massmail_OverviewController extends \AuthenticatedController
+{
+
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ if (!\MassMail\MassMailPermission::has(User::findCurrent()->id)) {
+ throw new AccessDeniedException();
+ }
+
+ Navigation::activateItem('/messaging/massmail/overview');
+
+ Sidebar::Get()->addWidget(new VueWidget('message-views'));
+
+ $this->render_vue_app(
+ Studip\VueApp::create('massmail/MassMailMessagesList')
+ );
+ }
+
+ public function index_action($id = null)
+ {
+ PageLayout::setTitle(_('Nachrichten'));
+
+ $this->render_vue_app(
+ Studip\VueApp::create('massmail/MassMailMessagesList')
+ );
+ }
+
+}
diff --git a/app/controllers/massmail/permissions.php b/app/controllers/massmail/permissions.php
new file mode 100644
index 0000000..bd8200b
--- /dev/null
+++ b/app/controllers/massmail/permissions.php
@@ -0,0 +1,174 @@
+<?php
+
+class Massmail_PermissionsController extends \AuthenticatedController
+{
+
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ if (!\MassMail\MassMailPermission::has(User::findCurrent()->id, true)) {
+ throw new AccessDeniedException();
+ }
+
+ Navigation::activateItem('/messaging/massmail/permissions');
+ }
+
+ /**
+ * Lists all existing permissions.
+ * @return void
+ * @throws AccessDeniedException
+ */
+ public function index_action()
+ {
+ PageLayout::setTitle(_('Berechtigungen für den Nachrichtenversand an Zielgruppen'));
+
+ $this->permissions = \MassMail\MassMailPermission::findBySQL("1");
+ usort(
+ $this->permissions,
+ fn ($a, $b) => strnatcasecmp($a->institute_name, $b->institute_name)
+ );
+
+ $sidebar = Sidebar::Get();
+ $actions = new ActionsWidget();
+ $actions->addLink(
+ _('Neue Berechtigung vergeben'),
+ $this->url_for('massmail/permissions/edit'),
+ Icon::create('add'),
+ )->asDialog('size=medium');
+ $sidebar->addWidget($actions);
+
+ $this->render_vue_app(
+ Studip\VueApp::create('massmail/MassMailPermissions')
+ );
+ }
+
+ /**
+ * Provides a form for entering or editing a massmail permission.
+ * @param int $id which permission to edit, create a new one if $id is 0
+ * @return void
+ * @throws AccessDeniedException
+ */
+ public function edit_action(int $id = 0)
+ {
+ $permission = new \MassMail\MassMailPermission($id);
+
+ PageLayout::setTitle(
+ $permission->isNew()
+ ? _('Berechtigung erstellen')
+ : _('Berechtigung bearbeiten')
+ );
+
+ $institutes = [];
+ foreach (Institute::getInstitutes() as $one) {
+ $institutes[$one['Institut_id']] = $one['Name'];
+ }
+
+ $degrees = [];
+ foreach (Degree::findBySQL("1 ORDER BY `name`") as $one) {
+ $degrees[$one->id] = $one->name;
+ }
+
+ $subjects = [];
+ foreach (StudyCourse::findBySQL("1 ORDER BY `name`") as $one) {
+ $subjects[$one->id] = $one->name;
+ }
+
+ $form = \Studip\Forms\Form::fromSORM(
+ $permission,
+ [
+ 'fields' => [
+ 'institute_id' => [
+ 'type' => 'select',
+ 'required' => true,
+ 'label' => _('Einrichtung'),
+ 'value' => $permission->institute_id,
+ 'options' => $institutes
+ ],
+ 'min_perm' => [
+ 'type' => 'select',
+ 'required' => true,
+ 'label' => _('Benötigte Rechte'),
+ 'value' => $permission->min_perm,
+ 'options' => [
+ 'admin' => 'admin',
+ 'dozent' => 'dozent',
+ 'tutor' => 'tutor'
+ ]
+ ],
+ 'allowed_degrees' => [
+ 'type' => 'checkboxCollection',
+ 'collapsable' => true,
+ 'label' => _('Erlaubte Abschlüsse'),
+ 'value' => $permission->allowed_degrees->pluck('id'),
+ 'options' => $degrees
+ ],
+ 'allowed_subjects' => [
+ 'type' => 'checkboxCollection',
+ 'collapsable' => true,
+ 'label' => _('Erlaubte Fächer'),
+ 'value' => $permission->allowed_subjects->pluck('id'),
+ 'options' => $subjects
+ ],
+ 'allowed_institutes' => [
+ 'type' => 'checkboxCollection',
+ 'collapsable' => true,
+ 'label' => _('Erlaubte Einrichtungen (außer den eigenen)'),
+ 'value' => $permission->allowed_institutes->pluck('id'),
+ 'options' => $institutes
+ ]
+ ]
+ ]
+ )->setURL($this->url_for('massmail/permissions/store', $id));
+
+ $this->render_form($form);
+ }
+
+ /**
+ * Stores permission data by editing an existing or creating a new one.
+ * @param int $id the permission to store
+ * @return void
+ * @throws AccessDeniedException
+ */
+ public function store_action(int $id = 0)
+ {
+ CSRFProtection::verifyUnsafeRequest();
+ $permission = new \MassMail\MassMailPermission($id);
+ $permission->institute_id = Request::option('institute_id');
+ $permission->min_perm = Request::get('min_perm');
+ $permission->allowed_degrees = Degree::findMany(Request::optionArray('allowed_degrees'));
+ $permission->allowed_subjects = StudyCourse::findMany(Request::optionArray('allowed_subjects'));
+ $permission->allowed_institutes = Institute::findMany(Request::optionArray('allowed_institutes'));
+
+ if ($permission->store() !== false) {
+ PageLayout::postSuccess('Die Daten wurden gespeichert.');
+ } else {
+ PageLayout::postError('Die Daten konnten nicht gespeichert werden.');
+ }
+
+ $this->relocate('massmail/permissions');
+ }
+
+ /**
+ * Deletes the given permission entry.
+ * @param int $id the permission to delete
+ * @return void
+ * @throws AccessDeniedException
+ */
+ public function delete_action(int $id)
+ {
+ $permission = \MassMail\MassMailPermission::find($id);
+ if ($permission) {
+ if ($permission->delete()) {
+ PageLayout::postSuccess(_('Die Berechtigung wurde gelöscht.'));
+ } else {
+ PageLayout::postError(_('Die Berechtigung konnte nicht gelöscht werden.'));
+ }
+ } else {
+ PageLayout::postError(_('Die Berechtigung wurde nicht gefunden.'));
+ }
+
+ $this->relocate('massmail/permissions');
+ }
+
+}
diff --git a/app/controllers/massmail/quick.php b/app/controllers/massmail/quick.php
new file mode 100644
index 0000000..b7cdf94
--- /dev/null
+++ b/app/controllers/massmail/quick.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * quick.php Controller for quick creation of massmails to selected courses.
+ *
+ * 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 Thomas Hackl
+ * @license GPL2 or any later version
+ * @since Stud.IP 6.0
+ */
+
+class Massmail_QuickController extends \AuthenticatedController
+{
+
+ public function courses_action()
+ {
+ $GLOBALS['perm']->check('admin');
+
+ Navigation::activateItem('/messaging/massmail/message');
+ PageLayout::setTitle(_('Nachricht an Zielgruppe schreiben'));
+
+ $message = new \MassMail\MassMailMessage();
+ $message->target = 'courses';
+ $message->sender_id = $message->author_id = User::findCurrent()->id;
+ $message->config = ['perm' => 'autor', 'courses' => Request::optionArray('courses')];
+
+ $courses = Request::optionArray('courses');
+
+ $form = \Studip\Forms\Form::fromSORM(
+ $message,
+ [
+ 'legend' => _('Grunddaten'),
+ 'collapsed' => false,
+ 'collapsable' => false,
+ 'fields' => [
+ 'courses' => [
+ 'type' => 'hidden',
+ 'value' => implode(',', $courses),
+ 'store' => function($value, $input) {
+ $input->getContextObject()->config = [];
+ $input->getContextObject()->config['courses'] = explode(',', $value);
+ }
+ ],
+ 'course_perm' => [
+ 'type' => 'select',
+ 'label' => _('Berechtigungsebene wählen'),
+ 'value' => 'autor',
+ 'options' => [
+ 'dozent' => get_title_for_status('dozent', 2, 1),
+ 'tutor' => get_title_for_status('tutor', 2, 1),
+ 'autor' => get_title_for_status('autor', 2, 1),
+ 'user' => get_title_for_status('user', 2, 1),
+ ],
+ 'store' => function($value, $input) {
+ $input->getContextObject()->config['perm'] = $value;
+ }
+ ],
+ 'subject' => [
+ 'type' => 'text',
+ 'required' => true,
+ 'label' => _('Betreff'),
+ 'value' => $message->subject
+ ],
+ 'message' => [
+ 'type' => 'serialWysiwyg',
+ 'required' => true,
+ 'label' => _('Nachricht'),
+ 'value' => $message->message,
+ 'markers' => json_encode(
+ array_map(
+ fn ($m) => $m->toArray(),
+ \MassMail\MassMailMarker::findAll(
+ \MassMail\MassMailPermission::has(User::findCurrent()->id, true)
+ )
+ )
+ )
+ ]
+ ]
+ ],
+ $this->url_for('admin/courses')
+ )->autoStore();
+
+ $this->render_form($form);
+ }
+
+}
diff --git a/app/controllers/massmail/settings.php b/app/controllers/massmail/settings.php
new file mode 100644
index 0000000..1b6c626
--- /dev/null
+++ b/app/controllers/massmail/settings.php
@@ -0,0 +1,109 @@
+<?php
+
+class Massmail_SettingsController extends \AuthenticatedController
+{
+
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ if (!\MassMail\MassMailPermission::has(User::findCurrent()->id, true)) {
+ throw new AccessDeniedException();
+ }
+
+ Navigation::activateItem('/messaging/massmail/settings');
+ }
+
+ /**
+ * Lists all existing permissions.
+ * @return void
+ * @throws AccessDeniedException
+ */
+ public function index_action()
+ {
+ PageLayout::setTitle(_('Einstellungen für den Nachrichtenversand an Zielgruppen'));
+
+ $categories = [];
+ foreach (SemClass::getClasses() as $class) {
+ $categories[$class['id']] = $class['name'];
+ }
+
+ $form = \Studip\Forms\Form::create();
+ $form->setURL($this->url_for('massmail/settings/store'));
+ $config = new \Studip\Forms\Fieldset(_('Konfiguration'));
+ $config->addInput(
+ new \Studip\Forms\NumberInput(
+ 'cleanup',
+ _('Anzahl Tage, nach denen bereits verschickte Nachrichten gelöscht werden (0 bedeutet nie)'),
+ Config::get()->MASSMAIL_GC_DAYS,
+ ['min' => 0]
+ )
+ );
+ $form->addPart($config);
+
+ $form->addInput(
+ new \Studip\Forms\CheckboxCollectionInput(
+ 'categories',
+ _('Veranstaltungskategorien, die für die Ermittlung aktiver Lehrender berücksichtigt werden'),
+ Config::get()->MASSMAIL_LECTURER_SEM_CATEGORIES,
+ ['options' => $categories]
+ )
+ );
+
+ $task = CronjobTask::findOneByClass(SendMassmailsJob::class);
+ $job = CronjobSchedule::findOneByTask_id($task->id);
+
+ $cron = new \Studip\Forms\Fieldset(_('Cronjob'));
+
+ if (!$task->active || !$job->active) {
+ $cron->addInput(
+ new \Studip\Forms\InfoInput(
+ 'inactive',
+ _('Achtung: Kein Versand'),
+ _('Der automatische Versand ist nicht aktiviert!')
+ )
+ );
+ }
+
+ $cron->addInput(
+ new \Studip\Forms\NumberInput(
+ 'minutes',
+ _('Abstand des Versands anstehender Nachrichten in Minuten'),
+ abs($job->minute),
+ ['min' => 1, 'max' => 59]
+ )
+ );
+ $form->addPart($cron);
+
+ $this->render_form($form);
+ }
+
+ /**
+ * Stores the global massmail settings..
+ * @return void
+ * @throws AccessDeniedException
+ */
+ public function store_action()
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ Config::get()->store(
+ 'MASSMAIL_GC_DAYS',
+ Request::int('cleanup', 7)
+ );
+ Config::get()->store(
+ 'MASSMAIL_LECTURER_SEM_CATEGORIES',
+ Request::intArray('categories')
+ );
+
+ $task = CronjobTask::findOneByClass(SendMassmailsJob::class);
+ $job = CronjobSchedule::findOneByTask_id($task->id);
+ $job->minute = -1 * abs(Request::int('minutes'));
+ $job->store();
+
+ PageLayout::postSuccess('Die Einstellungen wurden gespeichert.');
+
+ $this->relocate('massmail/settings');
+ }
+
+}
diff --git a/app/views/admin/courses/massmail.php b/app/views/admin/courses/massmail.php
new file mode 100644
index 0000000..e871040
--- /dev/null
+++ b/app/views/admin/courses/massmail.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * @var Course $course
+ */
+?>
+<label>
+ <input name="courses[]" type="checkbox" value="<?= htmlReady($course->id) ?>"
+ aria-label="<?= htmlReady(sprintf(_('Nachricht an Teilnehmende der Veranstaltung %s senden'),
+ $course->getFullName())) ?>">
+</label>
diff --git a/db/migrations/6.0.32_integrate_garuda_plugin.php b/db/migrations/6.0.32_integrate_garuda_plugin.php
new file mode 100644
index 0000000..2e08614
--- /dev/null
+++ b/db/migrations/6.0.32_integrate_garuda_plugin.php
@@ -0,0 +1,285 @@
+<?php
+require_once 'lib/models/MassMail/MassMailPermission.php';
+require_once 'lib/cronjobs/send_massmails.php';
+
+return new class extends Migration
+{
+ use DatabaseMigrationTrait;
+
+ public function description()
+ {
+ return 'Integrate the functionality from the Garuda plugin into the Stud.IP core.';
+ }
+
+ protected function up()
+ {
+ // Messages and templates
+ DBManager::get()->exec("CREATE TABLE IF NOT EXISTS `massmail_messages` (
+ `message_id` INT NOT NULL AUTO_INCREMENT,
+ `sender_id` CHAR(32) COLLATE latin1_bin,
+ `author_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+ `send_at_date` INT,
+ `target` ENUM('all', 'students', 'employees', 'lecturers', 'courses', 'usernames') COLLATE latin1_bin,
+ `config` LONGTEXT,
+ `exclude_users` LONGTEXT,
+ `cc` TEXT,
+ `subject` VARCHAR(255) NOT NULL,
+ `message` TEXT NOT NULL,
+ `folder_id` CHAR(32) COLLATE latin1_bin,
+ `is_template` TINYINT(1) NOT NULL DEFAULT 0,
+ `locked` TINYINT(1) NOT NULL DEFAULT 0,
+ `sent` TINYINT(1) NOT NULL DEFAULT 0,
+ `protected` TINYINT(1) NOT NULL DEFAULT 0,
+ `mkdate` INT UNSIGNED NOT NULL,
+ `chdate` INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`message_id`),
+ INDEX author_id (`author_id`)
+ )");
+
+ // Permissions for using this functionality
+ DBManager::get()->exec("CREATE TABLE IF NOT EXISTS `massmail_permissions` (
+ `permission_id` INT NOT NULL AUTO_INCREMENT,
+ `institute_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+ `min_perm` ENUM ('admin', 'dozent', 'tutor', 'autor') COLLATE latin1_bin NOT NULL DEFAULT 'admin',
+ `mkdate` INT UNSIGNED NOT NULL,
+ `chdate` INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`permission_id`),
+ UNIQUE INDEX institute_id (`institute_id`)
+ )");
+
+ // Allowed degrees
+ DBManager::get()->exec("CREATE TABLE IF NOT EXISTS `massmail_permission_degree` (
+ `permission_id` INT NOT NULL,
+ `degree_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+ `mkdate` INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`permission_id`, `degree_id`),
+ INDEX degree_id (`degree_id`)
+ )");
+
+ // Allowed subjects of study
+ DBManager::get()->exec("CREATE TABLE IF NOT EXISTS `massmail_permission_subject` (
+ `permission_id` INT NOT NULL,
+ `subject_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+ `mkdate` INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`permission_id`, `subject_id`),
+ INDEX subject_id (`subject_id`)
+ )");
+
+ // Allowed institutes
+ DBManager::get()->exec("CREATE TABLE IF NOT EXISTS `massmail_permission_institute` (
+ `permission_id` INT NOT NULL,
+ `institute_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+ `mkdate` INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`permission_id`, `institute_id`),
+ INDEX institute_id (`institute_id`)
+ )");
+
+ // User filters
+ DBManager::get()->exec("CREATE TABLE IF NOT EXISTS `massmail_filter` (
+ `message_id` INT NOT NULL,
+ `filter_id` CHAR(32) COLLATE latin1_bin NOT NULL,
+ `mkdate` INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`message_id`, `filter_id`),
+ INDEX filter_id (`filter_id`)
+ )");
+
+ // User-specific tokens
+ DBManager::get()->exec("CREATE TABLE IF NOT EXISTS `massmail_tokens` (
+ `token_id` INT NOT NULL AUTO_INCREMENT,
+ `message_id` INT NOT NULL,
+ `user_id` CHAR(32) COLLATE latin1_bin,
+ `token` VARCHAR(1024) NOT NULL,
+ `mkdate` INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`token_id`),
+ INDEX message_id (`message_id`)
+ )");
+
+ // Serial mail markers
+ DBManager::get()->exec("CREATE TABLE IF NOT EXISTS `massmail_markers` (
+ `marker_id` INT NOT NULL AUTO_INCREMENT,
+ `marker` VARCHAR(255) NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `type` ENUM('text', 'database', 'function', 'token') COLLATE latin1_bin,
+ `description` TEXT,
+ `root_only` TINYINT(1) UNSIGNED DEFAULT 0,
+ `replacement` TEXT,
+ `replacement_female` TEXT,
+ `replacement_unknown` TEXT,
+ `position` TINYINT(1) UNSIGNED,
+ `mkdate` INT UNSIGNED NOT NULL,
+ `chdate` INT UNSIGNED NOT NULL,
+ PRIMARY KEY (`marker_id`)
+ )");
+
+ $markers = [
+ [
+ 'marker' => 'FULLNAME',
+ 'name' => 'Voller Name',
+ 'type' => 'database',
+ 'description' => _('Hier wird der volle Name der jeweiligen Person eingesetzt, z.B. "Prof. Max Mustermann, PhD".'),
+ 'replacement' => 'user_info.title_front {{FIRSTNAME}} {{LASTNAME}} user_info.title_rear',
+ 'position' => 2
+ ],
+ [
+ 'marker' => 'FIRSTNAME',
+ 'name' => 'Vorname',
+ 'type' => 'database',
+ 'description' => _('Hier wird der Vorname der jeweiligen Person eingesetzt.'),
+ 'replacement' => 'auth_user_md5.Vorname',
+ 'position' => 3
+ ],
+ [
+ 'marker' => 'LASTNAME',
+ 'name' => 'Nachname',
+ 'type' => 'database',
+ 'description' => _('Hier wird der Nachname der jeweiligen Person eingesetzt.'),
+ 'replacement' => 'auth_user_md5.Nachname',
+ 'position' => 4
+ ],
+ [
+ 'marker' => 'USERNAME',
+ 'name' => 'Benutzername',
+ 'type' => 'database',
+ 'description' => _('Hier wird der Benutzername der jeweiligen Person eingesetzt.'),
+ 'replacement' => 'auth_user_md5.username',
+ 'position' => 5
+ ],
+ [
+ 'marker' => 'SEHRGEEHRTE',
+ 'name' => 'Anrede mit vollem Namen',
+ 'type' => 'text',
+ 'description' => _('Hier wird eine Anrede erzeugt: "Sehr geehrte Michaela Musterfrau" bzw. "Sehr geehrter Max Mustermann".'),
+ 'replacement' => 'Sehr geehrter {{FULLNAME}}',
+ 'replacement_female' => 'Sehr geehrte {{FULLNAME}}',
+ 'replacement_unknown' => 'Sehr geehrte/r {{FULLNAME}}',
+ 'position' => 1
+ ],
+ [
+ 'marker' => 'DEARSIRMADAM',
+ 'name' => 'Anrede (englisch) mit vollem Namen',
+ 'type' => 'text',
+ 'description' => _('Creates a Salutation: "Dear Jane Doe" or "Dear John Doe".'),
+ 'replacement' => 'Dear {{FULLNAME}}',
+ 'position' => 6
+ ],
+ [
+ 'marker' => 'TOKEN',
+ 'name' => 'Personalisierter Code o.ä.',
+ 'type' => 'token',
+ 'description' => _('Hier wird ein persönlicher Teilnahmecode o.ä. aus einer hochgeladenen Datei eingesetzt.'),
+ 'replacement' => 'massmail_tokens.token',
+ 'root_only' => 1,
+ 'position' => 7
+ ]
+ ];
+
+ foreach ($markers as $data) {
+ \MassMail\MassMailMarker::create($data);
+ }
+
+ if (empty(RolePersistence::getRoleIdByName(\MassMail\MassMailPermission::MASSMAIL_ROOT_ROLE))) {
+ RolePersistence::saveRole(
+ new Role(Role::UNKNOWN_ROLE_ID, \MassMail\MassMailPermission::MASSMAIL_ROOT_ROLE)
+ );
+ }
+
+ DBManager::get()->exec("INSERT IGNORE INTO `config`
+ (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`)
+ VALUES
+ (
+ 'MASSMAIL_LECTURER_SEM_CATEGORIES',
+ '[1]',
+ 'array',
+ 'global',
+ 'MassMail',
+ UNIX_TIMESTAMP(),
+ UNIX_TIMESTAMP(),
+ 'Veranstaltungskategorien, die für die Ermittlung aktiver Lehrender berücksichtigt werden'
+ )"
+ );
+ DBManager::get()->exec("INSERT IGNORE INTO `config`
+ (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`)
+ VALUES
+ (
+ 'MASSMAIL_GC_DAYS',
+ '7',
+ 'integer',
+ 'global',
+ 'MassMail',
+ UNIX_TIMESTAMP(),
+ UNIX_TIMESTAMP(),
+ 'Anzahl Tage, nach denen bereits verschickte Nachrichten aus der Datenbank entfernt werden (0 bedeutet nie)'
+ )"
+ );
+
+ SendMassmailsJob::register()->schedulePeriodic(-15)->activate();
+
+ /*
+ * Extend userfilter table so that we know from which context a specific UserFilter comes from,
+ * allowing us to check permissions for editing.
+ */
+ if (!$this->columnExists('userfilter', 'range_id') && !$this->columnExists('userfilter', 'range_type')) {
+ DBManager::get()->exec("ALTER TABLE `userfilter`
+ ADD `range_id` VARCHAR(32) COLLATE `latin1_bin` NOT NULL AFTER `filter_id`,
+ ADD `range_type` VARCHAR(255) COLLATE `latin1_bin` NOT NULL AFTER `range_id`");
+ }
+
+ /*
+ * Set context values for existing userfilters (we only need to consider filters from admission rules
+ * as only those are part of the core so far)
+ */
+
+ // First: filters from ConditionalAdmissions
+ $conditions = DBManager::get()->fetchAll(
+ "SELECT DISTINCT c.`filter_id`, r.`set_id` FROM `admission_condition` c
+ JOIN `courseset_rule` r USING (`rule_id`)"
+ );
+ // Second: filters from PreferentialAdmissions
+ $preferential = DBManager::get()->fetchAll(
+ "SELECT DISTINCT p.`condition_id` AS filter_id, r.`set_id` FROM `prefadmission_condition` p
+ JOIN `courseset_rule` r USING (`rule_id`)"
+ );
+ foreach (array_merge($conditions, $preferential) as $filter) {
+ DBManager::get()->execute(
+ "UPDATE `userfilter` SET `range_id` = :range, `range_type` = :type WHERE `filter_id` = :filter",
+ ['range' => $filter['set_id'], 'type' => CourseSet::class, 'filter' => $filter['filter_id']]
+ );
+ }
+ }
+
+ protected function down()
+ {
+ $tables = [
+ 'massmail_messages',
+ 'massmail_permissions',
+ 'massmail_permission_degree',
+ 'massmail_permission_subject',
+ 'massmail_permission_institute',
+ 'massmail_filter',
+ 'massmail_tokens',
+ 'massmail_markers'
+ ];
+ DBManager::get()->execute(
+ "DROP TABLE IF EXISTS `" . implode('`,`', $tables) . "`");
+
+ $id = RolePersistence::getRoleIdByName(\MassMail\MassMailPermission::MASSMAIL_ROOT_ROLE);
+ if (!empty($id)) {
+ RolePersistence::deleteRole(new Role($id));
+ }
+
+ DBManager::get()->execute(
+ "DELETE FROM `config_values` WHERE `field` = :field",
+ ['field' => 'MASSMAIL_LECTURER_SEM_CATEGORIES']
+ );
+ DBManager::get()->execute(
+ "DELETE FROM `config` WHERE `field` = :field",
+ ['field' => 'MASSMAIL_LECTURER_SEM_CATEGORIES']
+ );
+
+ SendMassmailsJob::unregister();
+
+ if ($this->columnExists('userfilter', 'range_id') && $this->columnExists('userfilter', 'range_type')) {
+ DBManager::get()->exec("ALTER TABLE `userfilter` DROP `range_id`, DROP `range_type`");
+ }
+ }
+};
diff --git a/lib/admissionrules/conditionaladmission/ConditionalAdmission.php b/lib/admissionrules/conditionaladmission/ConditionalAdmission.php
index ab26cc2..10bcb99 100644
--- a/lib/admissionrules/conditionaladmission/ConditionalAdmission.php
+++ b/lib/admissionrules/conditionaladmission/ConditionalAdmission.php
@@ -446,6 +446,7 @@ class ConditionalAdmission extends AdmissionRule
$groupqueries = [];
$groupparameters = [];
foreach ($this->ungrouped_conditions as $condition) {
+ $condition->setRange(CourseSet::class, $this->courseSetId);
// Store each ungrouped condition...
$condition->store();
$queries[] = "(?, ?, ?, ?)";
@@ -460,6 +461,7 @@ class ConditionalAdmission extends AdmissionRule
$groupparameters[] = $conditiongroup_id;
$groupparameters[] = $this->quota[$conditiongroup_id];
foreach ($conditions as $condition) {
+ $condition->setRange(CourseSet::class, $this->courseSetId);
// Store each group of conditions...
$condition->store();
$queries[] = "(?, ?, ?, ?)";
diff --git a/lib/admissionrules/preferentialadmission/PreferentialAdmission.php b/lib/admissionrules/preferentialadmission/PreferentialAdmission.php
index 23633d6..9617f2b 100644
--- a/lib/admissionrules/preferentialadmission/PreferentialAdmission.php
+++ b/lib/admissionrules/preferentialadmission/PreferentialAdmission.php
@@ -487,6 +487,7 @@ class PreferentialAdmission extends AdmissionRule
$parameters = [];
if ($this->conditions) {
foreach ($this->conditions as $condition) {
+ $condition->setRange(CourseSet::class, $this->courseSetId);
// Store each condition...
$condition->store();
$queries[] = "(?, ?, ?)";
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index b13887f..ec3668d 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -133,6 +133,7 @@ class RouteMap
$this->addAuthenticatedForumRoutes($group);
$this->addAuthenticatedInstitutesRoutes($group);
$this->addAuthenticatedLtiRoutes($group);
+ $this->addAuthenticatedMassMailRoutes($group);
$this->addAuthenticatedMessagesRoutes($group);
$this->addAuthenticatedNewsRoutes($group);
$this->addAuthenticatedStockImagesRoutes($group);
@@ -305,6 +306,14 @@ class RouteMap
$group->get('/lti-tools', Routes\Lti\LtiToolsIndex::class);
}
+
+ private function addAuthenticatedMassMailRoutes(RouteCollectorProxy $group): void
+ {
+ $group->get('/mass-mails/messages', Routes\MassMail\MassMailMessagesIndex::class);
+ $group->get('/mass-mails/permissions', Routes\MassMail\MassMailPermissionsIndex::class);
+ $group->get('/mass-mails/permissions/{id}', Routes\MassMail\MassMailPermissionsShow::class);
+ }
+
private function addAuthenticatedNewsRoutes(RouteCollectorProxy $group): void
{
$group->post('/courses/{id}/news', Routes\News\CourseNewsCreate::class);
diff --git a/lib/classes/JsonApi/Routes/MassMail/Authority.php b/lib/classes/JsonApi/Routes/MassMail/Authority.php
new file mode 100644
index 0000000..20a79ce
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/MassMail/Authority.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace JsonApi\Routes\MassMail;
+
+use MassMail\MassMailPermission;
+use User;
+
+class Authority
+{
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public static function canShowMassMailPermissions(User $user, MassMailPermission $permission): bool
+ {
+ return MassMailPermission::has($user->id, true);
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public static function canIndexMassMailPermissions(User $user): bool
+ {
+ return MassMailPermission::has($user->id, true);
+ }
+
+ public static function canIndexMassMailMessages(User $user): bool
+ {
+ return MassMailPermission::has($user->id);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/MassMail/MassMailMessagesIndex.php b/lib/classes/JsonApi/Routes/MassMail/MassMailMessagesIndex.php
new file mode 100644
index 0000000..46f2c98
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/MassMail/MassMailMessagesIndex.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace JsonApi\Routes\MassMail;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use MassMail\MassMailMessage;
+use MassMail\MassMailPermission;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use JsonApi\JsonApiController;
+
+class MassMailMessagesIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = ['offset', 'limit'];
+ protected $allowedFilteringParameters = ['templates', 'queued', 'protected', 'locked', 'sent'];
+ protected $allowedIncludePaths = ['author', 'sender', 'filters'];
+
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ if (!Authority::canIndexMassMailMessages($this->getUser($request))) {
+ throw new AuthorizationFailedException();
+ }
+
+ $filters = $this->getContextFilters();
+
+ [$offset, $limit] = $this->getOffsetAndLimit();
+
+ $sql = "`is_template` = :template AND `locked` = :locked AND `sent` = :sent ".
+ "ORDER BY `chdate` DESC";
+ $parameters = [
+ 'template' => $filters['templates'] ? 1 : 0,
+ 'locked' => $filters['locked'] ? 1 : 0,
+ 'sent' => $filters['sent'] ? 1 : 0
+ ];
+
+ if ($filters['protected']) {
+ $sql = "`protected` = :protected AND " . $sql;
+ $parameters['protected'] = 1;
+ }
+
+ if (!MassMailPermission::has($this->getUser($request)->id, true) || $filters['templates']) {
+ $sql = "`author_id` = :author AND " . $sql;
+ $parameters['author'] = $this->getUser($request)->id;
+ }
+
+ $total = MassMailMessage::countBySQL($sql, $parameters);
+ $messages = MassMailMessage::findBySQL(
+ $sql . " LIMIT :limit OFFSET :offset",
+ array_merge(
+ $parameters,
+ ['limit' => $limit,'offset' => $offset]
+ ));
+
+ return $this->getPaginatedContentResponse($messages, $total);
+ }
+
+ private function getContextFilters()
+ {
+ $defaults = [
+ 'templates' => false,
+ 'queued' => false,
+ 'protected' => false,
+ 'locked' => false,
+ 'sent' => false
+ ];
+
+ $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+
+ return array_merge($defaults, $filtering);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsIndex.php b/lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsIndex.php
new file mode 100644
index 0000000..1d8f3ab
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsIndex.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace JsonApi\Routes\MassMail;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use JsonApi\JsonApiController;
+
+class MassMailPermissionsIndex extends JsonApiController
+{
+ protected $allowedPagingParameters = ['offset', 'limit'];
+ protected $allowedIncludePaths = ['institute', 'allowed-degrees', 'allowed-subjects', 'allowed-institutes'];
+
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ if (!Authority::canIndexMassMailPermissions($this->getUser($request))) {
+ throw new AuthorizationFailedException();
+ }
+
+ [$offset, $limit] = $this->getOffsetAndLimit();
+
+ $total = \MassMail\MassMailPermission::countBySQL('1');
+ $permissions = \MassMail\MassMailPermission::findBySQL(
+ "JOIN `Institute` ON (`Institute`.`Institut_id` = `massmail_permissions`.`institute_id`)
+ ORDER BY `Institute`.`Name` LIMIT ?, ?",
+ [$offset, $limit]);
+
+ return $this->getPaginatedContentResponse($permissions, $total);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsShow.php b/lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsShow.php
new file mode 100644
index 0000000..1f91b05
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsShow.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace JsonApi\Routes\MassMail;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+/**
+ * Displays settings for the given massmail permissions..
+ */
+class MassMailPermissionsShow extends JsonApiController
+{
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function __invoke(Request $request, Response $response, $args)
+ {
+ if (!$resource = \MassMail\MassMailPermission::find($args['id'])) {
+ throw new RecordNotFoundException();
+ }
+
+ if (!Authority::canShowMassMailPermissions($this->getUser($request), $resource)) {
+ throw new AuthorizationFailedException();
+ }
+
+
+ return $this->getContentResponse($resource);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/UserFilters/Authority.php b/lib/classes/JsonApi/Routes/UserFilters/Authority.php
index d934894..e84e4d0 100644
--- a/lib/classes/JsonApi/Routes/UserFilters/Authority.php
+++ b/lib/classes/JsonApi/Routes/UserFilters/Authority.php
@@ -2,17 +2,12 @@
namespace JsonApi\Routes\UserFilters;
-use Config;
-use User;
+use Config, User, UserFilter;
class Authority
{
- public static function canEditUserFilters(User $user): bool
+ public static function canEditUserFilters(User $user, UserFilter $filter): bool
{
- return $GLOBALS['perm']->have_perm('admin', $user->id)
- || (
- Config::get()->ALLOW_DOZENT_COURSESET_ADMIN
- && $GLOBALS['perm']->have_perm('dozent', $user->id)
- );
+ return $filter->canEdit($user);
}
}
diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php b/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php
index ede43cf..d4e9efd 100644
--- a/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php
+++ b/lib/classes/JsonApi/Routes/UserFilters/UserFilterFieldsIndex.php
@@ -4,17 +4,27 @@ namespace JsonApi\Routes\UserFilters;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\BadRequestException;
use JsonApi\JsonApiController;
class UserFilterFieldsIndex extends JsonApiController
{
+ protected $allowedFilteringParameters = ['context', 'target'];
+
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameters)
*/
public function __invoke(Request $request, Response $response, $args)
{
+ $error = $this->validateFilters();
+ if ($error) {
+ throw new BadRequestException($error);
+ }
+
+ $filters = $this->getContextFilters();
+
$fields = [];
- foreach (\UserFilterField::getAvailableFilterFields() as $class => $name) {
+ foreach (\UserFilterField::getAvailableFilterFields($filters['context'], $filters['target']) as $class => $name) {
// Generic datafield conditions must be handled differently.
if (str_contains($class, '_')) {
[$classname, $typeparam] = explode('_', $class);
@@ -27,4 +37,30 @@ class UserFilterFieldsIndex extends JsonApiController
return $this->getContentResponse($fields);
}
+ private function validateFilters()
+ {
+ $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+
+ // context aka namespace filter
+ if (
+ isset($filtering['context'])
+ && !file_exists(
+ $GLOBALS['STUDIP_BASE_PATH'] . '/lib/classes/UserFilterFields/' . $filtering['context']
+ )
+ ) {
+ return 'Requested context user filters do not exist.';
+ }
+ }
+
+ private function getContextFilters()
+ {
+ $defaults = [
+ 'context' => '',
+ 'target' => ''
+ ];
+
+ $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+
+ return array_merge($defaults, $filtering);
+ }
}
diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php
index 42cd583..e57dc13 100644
--- a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php
+++ b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersCreate.php
@@ -20,16 +20,16 @@ class UserFiltersCreate extends JsonApiController
*/
public function __invoke(Request $request, Response $response, $args)
{
+ $filter = new \UserFilter();
+ $filter->show_user_count = true;
+
$json = $this->validate($request);
$user = $this->getUser($request);
- if (!Authority::canEditUserFilters($user)) {
+ if (!Authority::canEditUserFilters($user, $filter)) {
throw new AuthorizationFailedException();
}
- $filter = new \UserFilter();
- $filter->show_user_count = true;
-
foreach (self::arrayGet($json, 'data.attributes.filters') as $one) {
$classname = '\\' . $one['attributes']['type'];
$field = !empty($one['attributes']['typeparam'])
diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php
index 6f2b0cb..e3cd534 100644
--- a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php
+++ b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersDelete.php
@@ -18,18 +18,18 @@ class UserFiltersDelete extends JsonApiController
*/
public function __invoke(Request $request, Response $response, $args)
{
- $user = $this->getUser($request);
-
- if (!Authority::canEditUserFilters($user)) {
- throw new AuthorizationFailedException();
- }
-
$filter = new \UserFilter($args['id']);
if ($filter['id'] !== $args['id']) {
throw new RecordNotFoundException();
}
+ $user = $this->getUser($request);
+
+ if (!Authority::canEditUserFilters($user, $filter)) {
+ throw new AuthorizationFailedException();
+ }
+
$filter->delete();
return $this->getCodeResponse(204);
diff --git a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php
index 309da9b..f9adecc 100644
--- a/lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php
+++ b/lib/classes/JsonApi/Routes/UserFilters/UserFiltersUpdate.php
@@ -21,21 +21,21 @@ class UserFiltersUpdate extends JsonApiController
*/
public function __invoke(Request $request, Response $response, $args)
{
- $user = $this->getUser($request);
-
- if (!Authority::canEditUserFilters($user)) {
- throw new AuthorizationFailedException();
- }
-
$filter = new \UserFilter($args['id']);
if ($filter['id'] !== $args['id']) {
throw new RecordNotFoundException();
}
+ $user = $this->getUser($request);
+
+ if (!Authority::canEditUserFilters($user, $filter)) {
+ throw new AuthorizationFailedException();
+ }
+
$json = $this->validate($request);
- $fields = $filter->getFields();
+ $filter->fields = [];
foreach (self::arrayGet($json, 'data.attributes.filters') as $one) {
$classname = '\\' . $one['attributes']['type'];
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index 44bfd04..50d6760 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -2,8 +2,6 @@
namespace JsonApi;
-use JsonApi\Schemas\Clipboard;
-
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
@@ -37,6 +35,7 @@ class SchemaMap
\CourseMember::class => Schemas\CourseMember::class,
\CourseDate::class => Schemas\CourseEvent::class,
\CourseExDate::class => Schemas\CourseEvent::class,
+ \Degree::class => Schemas\Degree::class,
\FeedbackElement::class => Schemas\FeedbackElement::class,
\FeedbackEntry::class => Schemas\FeedbackEntry::class,
\JsonApi\Models\ForumCat::class => Schemas\ForumCategory::class,
@@ -44,6 +43,8 @@ class SchemaMap
\Institute::class => Schemas\Institute::class,
\InstituteMember::class => Schemas\InstituteMember::class,
\LtiTool::class => Schemas\LtiTool::class,
+ \MassMail\MassMailMessage::class => Schemas\MassMailMessage::class,
+ \MassMail\MassMailPermission::class => Schemas\MassMailPermission::class,
\Message::class => Schemas\Message::class,
\SemClass::class => Schemas\SemClass::class,
\Semester::class => Schemas\Semester::class,
@@ -64,7 +65,6 @@ class SchemaMap
\FolderType::class => Schemas\Folder::class,
\UserFilter::class => Schemas\UserFilter::class,
\UserFilterField::class => Schemas\UserFilterField::class,
-
\Courseware\Block::class => Schemas\Courseware\Block::class,
\Courseware\BlockComment::class => Schemas\Courseware\BlockComment::class,
\Courseware\BlockFeedback::class => Schemas\Courseware\BlockFeedback::class,
diff --git a/lib/classes/JsonApi/Schemas/Degree.php b/lib/classes/JsonApi/Schemas/Degree.php
new file mode 100644
index 0000000..05a3080
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Degree.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class Degree extends SchemaProvider
+{
+ const TYPE = 'degrees';
+
+ const REL_AUTHOR = 'author';
+ const REL_EDITOR = 'editor';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'name' => $resource->name,
+ 'shortname' => $resource->name_kurz,
+ 'description' => $resource->beschreibung,
+ 'mkdate' => date('c', $resource->mkdate),
+ 'chdate' => date('c', $resource->chdate)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $relationships = $this->getAuthorRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_AUTHOR));
+ $relationships = $this->getEditorRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_EDITOR));
+
+ return $relationships;
+ }
+
+ private function getAuthorRelationship(array $relationships, \Degree $degree, $includeData)
+ {
+ $author = \User::find($degree->author_id);
+
+ if ($author) {
+ $relationships[self::REL_AUTHOR] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($author),
+ ]
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_AUTHOR][self::RELATIONSHIP_DATA] = $author;
+ }
+ }
+
+ return $relationships;
+ }
+
+ private function getEditorRelationship(array $relationships, \Degree $degree, $includeData)
+ {
+ $editor = \User::find($degree->editor_id);
+
+ if ($editor) {
+ $relationships[self::REL_EDITOR] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($editor),
+ ]
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_EDITOR][self::RELATIONSHIP_DATA] = $editor;
+ }
+ }
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/JsonApi/Schemas/MassMailMessage.php b/lib/classes/JsonApi/Schemas/MassMailMessage.php
new file mode 100644
index 0000000..598b0f6
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/MassMailMessage.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class MassMailMessage extends SchemaProvider
+{
+ const TYPE = 'mass-mail-messages';
+ const REL_FILTERS = 'filters';
+ const REL_SENDER = 'sender';
+ const REL_AUTHOR = 'author';
+ const REL_RECIPIENTS = 'recipients';
+ const REL_FOLDER = 'folder';
+ const REL_TOKENS = 'tokens';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ return [
+ 'send-date' => date('d.m.Y H:i', $resource->send_at_date),
+ 'target' => \MassMail\MassMailMessage::getTargets()[$resource->target],
+ 'config' => $resource->config,
+ 'exclude-users' => $resource->exclude_users,
+ 'cc' => $resource->cc,
+ 'subject' => (string) $resource->subject,
+ 'message' => (string) $resource->message,
+ 'is-template' => (bool) $resource->is_template,
+ 'locked' => (bool) $resource->locked,
+ 'sent' => (bool) $resource->sent,
+ 'protected' => (bool) $resource->protected,
+ 'mkdate' => date('d.m.Y H:i', $resource->mkdate),
+ 'chdate' => date('d.m.Y H:i', $resource->chdate)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $relationships = $this->getAuthorRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_AUTHOR));
+ $relationships = $this->getSenderRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SENDER));
+
+ return $relationships;
+ }
+
+ private function getAuthorRelationship(array $relationships, \MassMail\MassMailMessage $message, $includeData)
+ {
+ $relationships[self::REL_AUTHOR] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($message->author),
+ ]
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_AUTHOR][self::RELATIONSHIP_DATA] = $message->author;
+ }
+
+ return $relationships;
+ }
+
+ private function getSenderRelationship(array $relationships, \MassMail\MassMailMessage $message, $includeData)
+ {
+ if ($message->sender_id && $message->sender) {
+ $relationships[self::REL_SENDER] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($message->sender),
+ ]
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_SENDER][self::RELATIONSHIP_DATA] = $message->sender;
+ }
+ }
+
+ return $relationships;
+ }
+
+}
diff --git a/lib/classes/JsonApi/Schemas/MassMailPermission.php b/lib/classes/JsonApi/Schemas/MassMailPermission.php
new file mode 100644
index 0000000..27150b4
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/MassMailPermission.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class MassMailPermission extends SchemaProvider
+{
+ const TYPE = 'mass-mail-permissions';
+ const REL_INSTITUTE = 'institute';
+ const REL_ALLOWED_DEGREES = 'allowed-degrees';
+ const REL_ALLOWED_SUBJECTS = 'allowed-subjects';
+ const REL_ALLOWED_INSTITUTES = 'allowed-institutes';
+
+ public function getId($resource): ?string
+ {
+ return $resource->id;
+ }
+
+ public function getAttributes($resource, ContextInterface $context): iterable
+ {
+ $user = $this->currentUser;
+
+ return [
+ 'min-perm' => $resource->min_perm,
+ 'mkdate' => date('c', $resource->mkdate),
+ 'chdate' => date('c', $resource->chdate)
+ ];
+ }
+
+ public function hasResourceMeta($resource): bool
+ {
+ return true;
+ }
+
+ /**
+ * @param \MassMail\MassMailPermission $resource
+ */
+ public function getResourceMeta($resource)
+ {
+ return [
+ 'allowed-degrees-count' => count($resource->allowed_degrees),
+ 'allowed-subjects-count' => count($resource->allowed_subjects),
+ 'allowed-institutes-count' => count($resource->allowed_institutes)
+ ];
+ }
+
+ public function getRelationships($resource, ContextInterface $context): iterable
+ {
+ $relationships = [];
+
+ $relationships = $this->getInstituteRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_INSTITUTE));
+ $relationships = $this->getAllowedDegreesRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_ALLOWED_DEGREES));
+ $relationships = $this->getAllowedSubjectsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_ALLOWED_SUBJECTS));
+ $relationships = $this->getAllowedInstitutesRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_ALLOWED_INSTITUTES));
+
+ return $relationships;
+ }
+
+ private function getInstituteRelationship(array $relationships, \MassMail\MassMailPermission $permission, $includeData)
+ {
+ $relationships[self::REL_INSTITUTE] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->createLinkToResource($permission->institute),
+ ]
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_INSTITUTE][self::RELATIONSHIP_DATA] = $permission->institute;
+ }
+
+ return $relationships;
+ }
+
+ private function getAllowedDegreesRelationship(array $relationships, \MassMail\MassMailPermission $permission, $includeData)
+ {
+
+ $relationships[self::REL_ALLOWED_DEGREES] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($permission, self::REL_ALLOWED_DEGREES),
+ ]
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_ALLOWED_DEGREES][self::RELATIONSHIP_DATA] = $permission->allowed_degrees;
+ }
+
+ return $relationships;
+ }
+
+ private function getAllowedSubjectsRelationship(array $relationships, \MassMail\MassMailPermission $permission, $includeData)
+ {
+ $relationships[self::REL_ALLOWED_SUBJECTS] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($permission, self::REL_ALLOWED_SUBJECTS),
+ ]
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_ALLOWED_SUBJECTS][self::RELATIONSHIP_DATA] = $permission->allowed_subjects;
+ }
+
+ return $relationships;
+ }
+
+ private function getAllowedInstitutesRelationship(array $relationships, \MassMail\MassMailPermission $permission, $includeData)
+ {
+ $relationships[self::REL_ALLOWED_INSTITUTES] = [
+ self::RELATIONSHIP_LINKS => [
+ Link::RELATED => $this->getRelationshipRelatedLink($permission, self::REL_ALLOWED_INSTITUTES),
+ ]
+ ];
+
+ if ($includeData) {
+ $relationships[self::REL_ALLOWED_INSTITUTES][self::RELATIONSHIP_DATA] = $permission->allowed_institutes;
+ }
+
+ return $relationships;
+ }
+}
diff --git a/lib/classes/admission/UserFilter.php b/lib/classes/UserFilter.php
index fd160d6..7745587 100644
--- a/lib/classes/admission/UserFilter.php
+++ b/lib/classes/UserFilter.php
@@ -16,7 +16,6 @@
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
-
class UserFilter
{
// --- ATTRIBUTES ---
@@ -31,6 +30,10 @@ class UserFilter
*/
public $id = '';
+ // Data about where this filter belongs.
+ public string $range_id = '';
+ public string $range_type = '';
+
public $show_user_count = false;
// --- OPERATIONS ---
@@ -38,12 +41,11 @@ class UserFilter
/**
* Standard constructor.
*
- * @param String conditionId
+ * @param String conditionId
* @return UserFilter
*/
- public function __construct($conditionId='')
+ public function __construct($conditionId = '')
{
- UserFilterField::getAvailableFilterFields();
$this->id = $conditionId;
if ($conditionId) {
$this->load();
@@ -56,7 +58,7 @@ class UserFilter
/**
* Add a new condition field.
*
- * @param ConditionField fieldId
+ * @param UserFilterField fieldId
* @return UserFilter
*/
public function addField($field)
@@ -69,7 +71,8 @@ class UserFilter
/**
* Deletes the condition and all associated fields.
*/
- public function delete() {
+ public function delete()
+ {
// Delete condition data.
$stmt = DBManager::get()->prepare("DELETE FROM `userfilter`
WHERE `filter_id`=?");
@@ -83,11 +86,12 @@ class UserFilter
/**
* Generate a new unique ID.
*
- * @param String tableName
+ * @param String tableName
*/
- public function generateId() {
+ public function generateId()
+ {
do {
- $newid = md5(uniqid(get_class($this).microtime(), true));
+ $newid = md5(uniqid(get_class($this) . microtime(), true));
$id = DBManager::get()->fetchColumn("SELECT `filter_id`
FROM `userfilter` WHERE `filter_id`=?", [$newid]);
} while ($id);
@@ -102,8 +106,8 @@ class UserFilter
*/
public function getFields()
{
- uasort($this->fields, function($a, $b) {
- return $a->sortOrder - $b->sortOrder;
+ uasort($this->fields, function ($a, $b) {
+ return $a->sortOrder - $b->sortOrder;
});
return $this->fields;
}
@@ -123,7 +127,8 @@ class UserFilter
*
* @return Array
*/
- public function getUsers() {
+ public function getUsers()
+ {
$users = null;
foreach ($this->fields as $field) {
// Check if restrictions for the field value must be taken into consideration.
@@ -142,7 +147,7 @@ class UserFilter
}
$users = isset($users) ? array_intersect($users, $field->getUsers($restrictions)) : $field->getUsers($restrictions);
}
- return (array) $users;
+ return (array)$users;
}
/**
@@ -152,7 +157,8 @@ class UserFilter
* @param String $className the type to check for
* @return UserFilterField Return the found field or null if not applicable.
*/
- public function hasField($className) {
+ public function hasField($className)
+ {
foreach ($this->fields as $field) {
if ($field instanceof $className) {
return $field;
@@ -182,13 +188,16 @@ class UserFilter
/**
* Helper function for loading data from DB.
*/
- public function load() {
+ public function load()
+ {
// Load basic condition data.
$stmt = DBManager::get()->prepare(
"SELECT * FROM `userfilter` WHERE `filter_id`=? LIMIT 1");
$stmt->execute([$this->id]);
if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
$this->id = $data['filter_id'];
+ $this->range_id = $data['range_id'];
+ $this->range_type = $data['range_type'];
// Load the associated condition fields.
$stmt = DBManager::get()->prepare(
"SELECT `field_id`, `type` FROM `userfilter_fields`
@@ -201,16 +210,16 @@ class UserFilter
* been removed since saving data to DB.
*/
//try {
- $chunks = explode('_', $data['type']);
- $type = $chunks[0];
- $param = $chunks[1] ?? null;
- if ($param) {
- $field = new $type($param, $data['field_id']);
- } else {
- $field = new $type($data['field_id']);
- }
+ $chunks = explode('_', $data['type']);
+ $type = $chunks[0];
+ $param = $chunks[1] ?? null;
+ if ($param) {
+ $field = new $type($param, $data['field_id']);
+ } else {
+ $field = new $type($data['field_id']);
+ }
- $this->fields[$field->getId()] = $field;
+ $this->fields[$field->getId()] = $field;
//} catch (Exception $e) {}
}
}
@@ -219,7 +228,7 @@ class UserFilter
/**
* Removes the field with the given ID from the condition fields.
*
- * @param String fieldId
+ * @param String fieldId
* @return UserFilter
*/
public function removeField($fieldId)
@@ -231,7 +240,8 @@ class UserFilter
/**
* Stores data to DB.
*/
- public function store() {
+ public function store()
+ {
// Generate new ID if condition entry doesn't exist in DB yet.
if (!$this->id) {
$this->id = $this->generateId();
@@ -239,34 +249,36 @@ class UserFilter
// Store condition data.
$stmt = DBManager::get()->prepare("INSERT INTO `userfilter`
- (`filter_id`, `mkdate`, `chdate`)
- VALUES (?, ?, ?)
- ON DUPLICATE KEY UPDATE `chdate`=VALUES(`chdate`)");
- $stmt->execute([$this->id, time(), time()]);
+ (`filter_id`, `range_id`, `range_type`, `mkdate`, `chdate`)
+ VALUES (?, ?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE `chdate` = VALUES(`chdate`), `range_type` = VALUES(`range_type`), `range_id` = VALUES(`range_id`)");
+ $stmt->execute([$this->id, $this->range_id, $this->range_type, time(), time()]);
// Delete removed condition fields from DB.
DBManager::get()->exec("DELETE FROM `userfilter_fields`
- WHERE `filter_id`='".$this->id."' AND `field_id` NOT IN ('".
- implode("', '", array_keys($this->fields))."')");
+ WHERE `filter_id`='" . $this->id . "' AND `field_id` NOT IN ('" .
+ implode("', '", array_keys($this->fields)) . "')");
// Store all fields.
foreach ($this->fields as $field) {
$field->store($this->id);
}
}
- public function toString() {
+ public function toString()
+ {
$tpl = $GLOBALS['template_factory']->open('userfilter/display');
$tpl->set_attribute('filter', $this);
return $tpl->render();
}
- public function __toString() {
+ public function __toString()
+ {
return $this->toString();
}
public function __clone()
{
$this->id = md5(uniqid(get_class($this)));
- $cloned_fields= [];
+ $cloned_fields = [];
foreach ($this->fields as $field) {
$dolly = clone $field;
$dolly->conditionId = $this->id;
@@ -275,6 +287,34 @@ class UserFilter
$this->fields = $cloned_fields;
}
+ /**
+ * Checks whether the given user can edit this filter.
+ * @return bool
+ */
+ public function canEdit(User $user): bool
+ {
+ // This is a new object, we can always create that as it has no other connection to the system or database.
+ if (!$this->range_type || !$this->range_id) {
+ return true;
+ }
+
+ // Check for an existing object, using range_type and tange_id.
+ $range = new $this->range_type($this->range_id);
+ return $range->canEditFilter($user, $this);
+ }
+
+ /**
+ * Sets the range this UserFilter belongs to.
+ * @param string $type
+ * @param string|int $id
+ * @return void
+ */
+ public function setRange(string $type, string|int $id): void
+ {
+ $this->range_type = $type;
+ $this->range_id = $id;
+ }
+
} /* end of class UserFilter */
?>
diff --git a/lib/classes/admission/UserFilterField.php b/lib/classes/UserFilterField.php
index 2a34807..d997489 100644
--- a/lib/classes/admission/UserFilterField.php
+++ b/lib/classes/UserFilterField.php
@@ -16,7 +16,6 @@
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
-
class UserFilterField
{
// --- ATTRIBUTES ---
@@ -55,7 +54,7 @@ class UserFilterField
* Provide some kind of sort order for filter fields. By default,
* all subclasses without an explicitly given order will be sorted at the end.
*/
- public $sortOrder = 99;
+ public static $sortOrder = 99;
public static $isParameterized = false;
@@ -80,6 +79,24 @@ class UserFilterField
}
+ /**
+ * Which targets are allowed for this filter field?
+ * An empty array means: no restrictions
+ * @return array
+ */
+ public static function getTargets()
+ {
+ return [];
+ }
+
+ /**
+ * Indicates whether this filter field is active.
+ * @return true
+ */
+ public static function isActive()
+ {
+ return true;
+ }
/**
* Standard constructor.
@@ -100,8 +117,8 @@ class UserFilterField
} else {
// Get all available values from database.
$stmt = DBManager::get()->query(
- "SELECT DISTINCT `" . $this->valuesDbIdField . "`, `" . $this->valuesDbNameField . "` " .
- "FROM `" . $this->valuesDbTable . "` ORDER BY `" . $this->valuesDbNameField . "` ASC");
+ "SELECT DISTINCT `" . $this->valuesDbIdField . "`, `" . $this->valuesDbNameField . "` " .
+ "FROM `" . $this->valuesDbTable . "` ORDER BY `" . $this->valuesDbNameField . "` ASC");
while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) {
$this->validValues[$current[$this->valuesDbIdField]] = $current[$this->valuesDbNameField];
}
@@ -121,7 +138,7 @@ class UserFilterField
* value is compared to the currently selected value by using the
* currently selected compare operator.
*
- * @param Array values
+ * @param Array values
* @return Boolean
*/
public function checkValue($values)
@@ -177,12 +194,12 @@ class UserFilterField
/**
* Generate a new unique ID.
*
- * @param String tableName
+ * @param String tableName
*/
public function generateId()
{
do {
- $newid = md5(uniqid(get_class($this).microtime(), true));
+ $newid = md5(uniqid(get_class($this) . microtime(), true));
$id = DBManager::get()->fetchColumn("SELECT `field_id`
FROM `userfilter_fields` WHERE `field_id`=?", [$newid]);
} while ($id);
@@ -192,23 +209,46 @@ class UserFilterField
/**
* Reads all available UserFilterField subclasses and loads their definitions.
*/
- public static function getAvailableFilterFields()
+ public static function getAvailableFilterFields(string $context = '', string $target = '')
{
if (self::$available_filter_fields === null) {
$fields = [];
$i = new FileSystemIterator(
- $GLOBALS['STUDIP_BASE_PATH'] . '/lib/classes/admission/userfilter',
+ $GLOBALS['STUDIP_BASE_PATH'] . '/lib/classes/UserFilterFields' . ($context !== '' ? '/' . $context : ''),
FileSystemIterator::SKIP_DOTS
);
foreach ($i as $class) {
- require_once $class;
+ if ($class->isFile()) {
+ require_once $class;
+ }
}
+ // Get all classes in given context.
$classes = array_filter(
get_declared_classes(),
- fn($c) => is_subclass_of($c, UserFilterField::class)
+ function ($c) use ($context) {
+ $reflection_class = new \ReflectionClass($c);
+ $namespace = $reflection_class->getNamespaceName();
+ return is_subclass_of($c, UserFilterField::class)
+ && $namespace === 'UserFilterFields' . ($context !== '' ? '\\' . $context : '')
+ && $c::isActive();
+ }
);
+
+ usort($classes, fn ($a, $b) => $a::$sortOrder - $b::$sortOrder);
+
+ // If a target is given, return only matching classes
+ if ($target !== '') {
+ $classes = array_filter(
+ $classes,
+ function ($c) use ($target) {
+ $targets = $c::getTargets();
+ return count($targets) === 0 || in_array($target, $targets);
+ }
+ );
+ }
+
foreach ($classes as $class) {
if ($class::$isParameterized) {
$fields = array_merge($fields, $class::getParameterizedTypes());
@@ -217,7 +257,6 @@ class UserFilterField
$fields[$class] = $filter->getName();
}
}
- asort($fields);
self::$available_filter_fields = $fields;
}
return self::$available_filter_fields;
@@ -281,10 +320,10 @@ class UserFilterField
$db = DBManager::get();
$users = [];
// Standard query getting the values without respecting other values.
- $select = "SELECT DISTINCT `".$this->userDataDbTable."`.`user_id` ";
- $from = "FROM `".$this->userDataDbTable."` ";
- $where = "WHERE `".$this->userDataDbTable."`.`".$this->userDataDbField.
- "`".$this->compareOperator."?";
+ $select = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` ";
+ $from = "FROM `" . $this->userDataDbTable . "` ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField .
+ "`" . $this->compareOperator . "?";
$parameters = [$this->value];
$joinedTables = [
$this->userDataDbTable => true
@@ -296,20 +335,20 @@ class UserFilterField
// Do we need to join in another table?
if (!$joinedTables[$restriction['table']]) {
$joinedTables[$restriction['table']] = true;
- $from .= " INNER JOIN `".$restriction['table']."` ON (`".
- $this->userDataDbTable."`.`".
- $this->relations[$otherField]['local_field']."`=`".
- $restriction['table']."`.`".
- $this->relations[$otherField]['foreign_field']."`)";
+ $from .= " INNER JOIN `" . $restriction['table'] . "` ON (`" .
+ $this->userDataDbTable . "`.`" .
+ $this->relations[$otherField]['local_field'] . "`=`" .
+ $restriction['table'] . "`.`" .
+ $this->relations[$otherField]['foreign_field'] . "`)";
}
// Expand WHERE statement with the value from restriction.
- $where .= " AND `".$restriction['table']."`.`".
- $restriction['field']."`".$restriction['compare']."?";
+ $where .= " AND `" . $restriction['table'] . "`.`" .
+ $restriction['field'] . "`" . $restriction['compare'] . "?";
$parameters[] = $restriction['value'];
}
}
// Get all the users that fulfill the condition.
- $stmt = $db->prepare($select.$from.$where);
+ $stmt = $db->prepare($select . $from . $where);
$stmt->execute($parameters);
while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) {
$users[] = $current['user_id'];
@@ -323,15 +362,15 @@ class UserFilterField
* for the user. These can then be compared with the required degrees
* whether they fit.
*
- * @param String $userId User to check.
- * @param array $additional conditions that are required for check.
+ * @param String $userId User to check.
+ * @param array $additional conditions that are required for check.
* @return array The value(s) for this user.
*/
public function getUserValues($userId, $additional = null)
{
$result = [];
- $query = "SELECT DISTINCT `".$this->userDataDbField."` ".
- "FROM `".$this->userDataDbTable."` ".
+ $query = "SELECT DISTINCT `" . $this->userDataDbField . "` " .
+ "FROM `" . $this->userDataDbTable . "` " .
"WHERE `user_id`=?";
$parameters = [$userId];
// Additional requirements given...
@@ -342,7 +381,7 @@ class UserFilterField
foreach ($additional as $a_condition) {
if ($a_condition->id != $this->id && $this->userDataDbTable == $a_condition->userDataDbTable &&
- !in_array($a_condition->userDataDbField, $usedFields)) {
+ !in_array($a_condition->userDataDbField, $usedFields)) {
$query .= " AND `" . $a_condition->userDataDbField . "` " . $a_condition->compareOperator . "?";
$parameters[] = $a_condition->value;
}
@@ -406,7 +445,7 @@ class UserFilterField
/**
* Sets a new selected compare operator
*
- * @param String newOperator
+ * @param String newOperator
* @return UserFilterField
*/
public function setCompareOperator($newOperator)
@@ -422,7 +461,7 @@ class UserFilterField
/**
* Connects the current field to a UserFilter.
*
- * @param String $id ID of a UserFilter object.
+ * @param String $id ID of a UserFilter object.
* @return UserFilterField
*/
public function setConditionId($id)
@@ -434,7 +473,7 @@ class UserFilterField
/**
* Sets a new selected value.
*
- * @param String newValue
+ * @param String newValue
* @return UserFilterField
*/
public function setValue($newValue)
@@ -450,7 +489,7 @@ class UserFilterField
/**
* Stores data to DB.
*
- * @param String conditionId The condition this field belongs to.
+ * @param String conditionId The condition this field belongs to.
*/
public function store()
{
diff --git a/lib/classes/admission/userfilter/DatafieldCondition.php b/lib/classes/UserFilterFields/DatafieldCondition.php
index 1bc93e8..9e7a2c3 100644
--- a/lib/classes/admission/userfilter/DatafieldCondition.php
+++ b/lib/classes/UserFilterFields/DatafieldCondition.php
@@ -12,22 +12,24 @@
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
-class DatafieldCondition extends UserFilterField
+namespace UserFilterFields;
+
+class DatafieldCondition extends \UserFilterField
{
public static $isParameterized = true;
public $datafield_id, $null_yields, $datafield_name;
- public $sortOrder = 6;
+ public static $sortOrder = 6;
public static function getParameterizedTypes()
{
$ret = [];
try {
- foreach (DataField::findBySQL("object_type='user' AND (object_class & (1|2|4|8) OR object_class IS NULL) AND is_userfilter = 1 ORDER BY priority") as $df) {
+ foreach (\DataField::findBySQL("object_type='user' AND (object_class & (1|2|4|8) OR object_class IS NULL) AND is_userfilter = 1 ORDER BY priority") as $df) {
$ret[__CLASS__ . '_' . $df->id] = utf8_encode(chr(160)) . _("Datenfeld") . ': ' . $df->name;
}
- } catch (PDOException $e) {} //migration 128 chokes on this...
+ } catch (\PDOException $e) {} //migration 128 chokes on this...
return $ret;
}
/**
@@ -49,26 +51,26 @@ class DatafieldCondition extends UserFilterField
$this->datafield_id = $typeparam;
}
- $df = DataField::find($this->datafield_id);
+ $df = \DataField::find($this->datafield_id);
if ($df) {
$this->datafield_name = $df->name;
} else {
- throw new UnexpectedValueException('datafield not found, id: ' . $typeparam);
+ throw new \UnexpectedValueException('datafield not found, id: ' . $typeparam);
}
- $typed_df = DataFieldEntry::createDataFieldEntry($df);
- if ($typed_df instanceof DataFieldBoolEntry) {
+ $typed_df = \DataFieldEntry::createDataFieldEntry($df);
+ if ($typed_df instanceof \DataFieldBoolEntry) {
$this->validValues = [1 => _('Ja'), 0 => _('Nein')];
unset($this->validCompareOperators['>=']);
unset($this->validCompareOperators['<=']);
unset($this->validCompareOperators['!=']);
$this->null_yields = 0;
- } else if ($typed_df instanceof DataFieldSelectboxEntry) {
+ } else if ($typed_df instanceof \DataFieldSelectboxEntry) {
list($valid_values, $is_assoc) = $typed_df->getParameters();
if (!$is_assoc) {
$valid_values = array_combine($valid_values, $valid_values);
}
$this->validValues = $valid_values;
- $this->null_yields = $typed_df instanceof DataFieldSelectboxMultipleEntry ? '' : key($valid_values);
+ $this->null_yields = $typed_df instanceof \DataFieldSelectboxMultipleEntry ? '' : key($valid_values);
} else {
$this->null_yields = '';
}
@@ -87,7 +89,7 @@ class DatafieldCondition extends UserFilterField
public function getUsers($restrictions = [])
{
- $db = DBManager::get();
+ $db = \DBManager::get();
// Standard query getting the values without respecting other values.
$select = "SELECT user_id FROM
auth_user_md5 LEFT JOIN
@@ -107,7 +109,7 @@ class DatafieldCondition extends UserFilterField
*/
public function getUserValues($userId, $additional = null)
{
- $result = DBManager::get()->fetchColumn(
+ $result = \DBManager::get()->fetchColumn(
"SELECT content FROM datafields_entries
WHERE datafield_id = ? AND range_id = ?", [$this->datafield_id, $userId]);
return [$result === null || $result === false ? $this->null_yields : $result];
@@ -118,10 +120,10 @@ class DatafieldCondition extends UserFilterField
*/
public function load()
{
- $stmt = DBManager::get()->prepare(
+ $stmt = \DBManager::get()->prepare(
"SELECT * FROM `userfilter_fields` WHERE `field_id`=? LIMIT 1");
$stmt->execute([$this->id]);
- if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ if ($data = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$this->conditionId = $data['filter_id'];
$this->value = $data['value'];
$this->compareOperator = $data['compare_op'];
@@ -152,7 +154,7 @@ class DatafieldCondition extends UserFilterField
$this->id = $this->generateId();
}
// Store field data.
- $stmt = DBManager::get()->prepare("INSERT INTO `userfilter_fields`
+ $stmt = \DBManager::get()->prepare("INSERT INTO `userfilter_fields`
(`field_id`, `filter_id`, `type`, `value`, `compare_op`,
`mkdate`, `chdate`) VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE `filter_id`=VALUES(`filter_id`),
diff --git a/lib/classes/admission/userfilter/DegreeCondition.php b/lib/classes/UserFilterFields/DegreeCondition.php
index 61ce456..6b26fb0 100644
--- a/lib/classes/admission/userfilter/DegreeCondition.php
+++ b/lib/classes/UserFilterFields/DegreeCondition.php
@@ -1,4 +1,5 @@
<?php
+
/**
* DegreeCondition.php
*
@@ -13,7 +14,9 @@
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
-class DegreeCondition extends UserFilterField
+namespace UserFilterFields;
+
+class DegreeCondition extends \UserFilterField
{
// --- ATTRIBUTES ---
public $valuesDbTable = 'abschluss';
@@ -22,7 +25,7 @@ class DegreeCondition extends UserFilterField
public $userDataDbTable = 'user_studiengang';
public $userDataDbField = 'abschluss_id';
- public $sortOrder = 1;
+ public static $sortOrder = 1;
/**
* @see UserFilterField::__construct
diff --git a/lib/classes/UserFilterFields/DomainCondition.php b/lib/classes/UserFilterFields/DomainCondition.php
new file mode 100644
index 0000000..125a16b
--- /dev/null
+++ b/lib/classes/UserFilterFields/DomainCondition.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * DomainCondition.php
+ *
+ * All conditions concerning the user domain in Stud.IP can be specified here.
+ *
+ * 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 Thomas Hackl <thomas.hackl@uni-passau.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+namespace UserFilterFields;
+
+class DomainCondition extends \UserFilterField
+{
+ // --- ATTRIBUTES ---
+ public $valuesDbTable = 'userdomains';
+ public $valuesDbIdField = 'userdomain_id';
+ public $valuesDbNameField = 'name';
+ public $userDataDbTable = 'user_userdomains';
+ public $userDataDbField = 'userdomain_id';
+
+ public static $sortOrder = 8;
+
+ public static function isActive()
+ {
+ return \UserDomain::countBySQL("1") > 0;
+ }
+
+ /**
+ * Get this field's display name.
+ *
+ * @return String
+ */
+ public function getName()
+ {
+ return _('Domäne');
+ }
+
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailDegreeFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailDegreeFilter.php
new file mode 100644
index 0000000..71640d4
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailDegreeFilter.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace UserFilterFields\MassMail;
+
+use UserFilterFields\DegreeCondition;
+use MassMail\MassMailPermission;
+use User;
+use DBManager;
+use PDO;
+
+class MassMailDegreeFilter extends DegreeCondition
+{
+ /**
+ * @see \UserFilterField::getTargets()
+ */
+ public static function getTargets()
+ {
+ return ['students'];
+ }
+
+ public function __construct($fieldId = '')
+ {
+ parent::__construct($fieldId);
+
+ if (!MassMailPermission::has(User::findCurrent()->id, true)) {
+ $this->validValues = [];
+
+ $permission = MassMailPermission::getForUser(User::findCurrent(), true);
+
+ foreach ($permission['allowed_degrees'] as [$id, $name]) {
+ $this->validValues[$id] = (string) $name;
+ }
+ }
+ }
+
+ public function getUsers($restrictions = [])
+ {
+ $users = [];
+
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $users = parent::getUsers($restrictions);
+ } else if (count($this->validValues) > 0) {
+ // Standard query getting the values without respecting other values.
+ $select = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` ";
+ $from = "FROM `" . $this->userDataDbTable . "` ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField .
+ "`" . $this->compareOperator . "?";
+ $parameters = [$this->value];
+ $joinedTables = [
+ $this->userDataDbTable => true
+ ];
+ // Check if there are restrictions given.
+ foreach ($restrictions as $otherField => $restriction) {
+ // We only take the value into consideration if it represents a valid restriction.
+ if ($this->relations[$otherField]) {
+ // Do we need to join in another table?
+ if (!$joinedTables[$restriction['table']]) {
+ $joinedTables[$restriction['table']] = true;
+ $from .= " INNER JOIN `" . $restriction['table'] . "` ON (`" .
+ $this->userDataDbTable . "`.`" .
+ $this->relations[$otherField]['local_field'] . "`=`" .
+ $restriction['table'] . "`.`" .
+ $this->relations[$otherField]['foreign_field'] . "`)";
+ }
+ // Expand WHERE statement with the value from restriction.
+ $where .= " AND `" . $restriction['table'] . "`.`" .
+ $restriction['field'] . "`" . $restriction['compare'] . "?";
+ $parameters[] = $restriction['value'];
+ }
+ }
+
+ $where .= " AND `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "` IN (?)";
+ $parameters[] = array_keys($this->validValues);
+
+ // Get all the users that fulfill the condition.
+ $users = \DBManager::get()->fetchFirst($select . $from . $where, $parameters);
+ }
+
+ return $users;
+ }
+
+ /**
+ * Gets the value for the given user that is relevant for this
+ * condition field. Here, this method looks up the study degree(s)
+ * for the user. These can then be compared with the required degrees
+ * whether they fit.
+ *
+ * @param String $userId User to check.
+ * @param array $additional conditions that are required for check.
+ * @return array The value(s) for this user.
+ */
+ public function getUserValues($userId, $additional = null)
+ {
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $result = parent::getUserValues($userId, $additional);
+ } else {
+ $result = [];
+ $query = "SELECT DISTINCT `" . $this->userDataDbField . "` " .
+ "FROM `" . $this->userDataDbTable . "` " .
+ "WHERE `user_id`=?";
+ $parameters = [$userId];
+ // Additional requirements given...
+ if (is_array($additional)) {
+
+ // Don't use the same database field twice as this can only get ugly.
+ $usedFields = [$this->userDataDbField];
+
+ foreach ($additional as $a_condition) {
+ if ($a_condition->id != $this->id && $this->userDataDbTable == $a_condition->userDataDbTable &&
+ !in_array($a_condition->userDataDbField, $usedFields)) {
+ $query .= " AND `" . $a_condition->userDataDbField . "` " . $a_condition->compareOperator . "?";
+ $parameters[] = $a_condition->value;
+ }
+ }
+ }
+ // Get semester of study for user.
+ $stmt = DBManager::get()->prepare($query);
+ $stmt->execute($parameters);
+ while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ $result[] = $current[$this->userDataDbField];
+ }
+ }
+ return $result;
+ }
+
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailDomainFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailDomainFilter.php
new file mode 100644
index 0000000..7d7a922
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailDomainFilter.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * DomainCondition.php
+ *
+ * All conditions concerning the user domain in Stud.IP can be specified here.
+ *
+ * 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 Thomas Hackl <thomas.hackl@uni-passau.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+namespace UserFilterFields\MassMail;
+
+use MassMail\MassMailPermission;
+use UserFilterFields\DomainCondition;
+use DBManager;
+use User;
+
+class MassMailDomainFilter extends DomainCondition
+{
+
+ public string $target = '';
+
+ /**
+ * Gets all users belonging to given domain.
+ *
+ * @return array All users that are affected by the current condition
+ * field.
+ */
+ public function getUsers($restrictions = [])
+ {
+ $users = [];
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $users = parent::getUsers($restrictions);
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ switch ($this->target) {
+ case 'employees':
+ $sql = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` FROM `" . $this->userDataDbTable
+ . "`JOIN `user_inst` USING (`user_id`) ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "`" . $this->compareOperator
+ . ":value AND `user_inst`.`Institut_id` IN (:institutes) AND `user_inst`.`inst_perms` IN (:perms)";
+ $parameters = [
+ 'value' => $this->value,
+ 'institutes' => $allowed['allowed_institutes'],
+ 'perms' => ['autor', 'tutor', 'dozent', 'admin']
+ ];
+ break;
+ case 'students':
+ default:
+ $sql = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` FROM `" . $this->userDataDbTable
+ . "`JOIN `user_studiengang` USING (`user_id`) ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "`" . $this->compareOperator
+ . ":value AND `user_studiengang`.`abschluss_id` IN (:degrees)
+ AND `user_studiengang`.`fach_id` IN (:subjects)";
+ $parameters = [
+ 'value' => $this->value,
+ 'degrees' => $allowed['allowed_degrees'],
+ 'subjects' => $allowed['allowed_subjects']
+ ];
+ break;
+ }
+ $users = DBManager::get()->fetchFirst($sql . $where, $parameters);
+ }
+
+ return $users;
+ }
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailGenderFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailGenderFilter.php
new file mode 100644
index 0000000..089fa95
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailGenderFilter.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace UserFilterFields\MassMail;
+
+use UserFilterField;
+use MassMail\MassMailPermission;
+use User;
+use DBManager;
+
+class MassMailGenderFilter extends UserFilterField
+{
+ public $userDataDbField = 'geschlecht';
+ public $userDataDbTable = 'user_info';
+
+ public static $sortOrder = 8;
+
+ public $target = '';
+
+ public function __construct($fieldId = '')
+ {
+ parent::__construct($fieldId);
+
+ $this->validCompareOperators = [
+ '=' => _('ist'),
+ '!=' => _('ist nicht'),
+ ];
+
+ $this->validValues = [
+ 0 => _('unbekannt'),
+ 1 => _('männlich'),
+ 2 => _('weiblich'),
+ 3 => _('divers')
+ ];
+ }
+
+ public function getName()
+ {
+ return _('Geschlecht');
+ }
+
+ /**
+ * Gets all users with given gender.
+ *
+ * @return array All users that are affected by the current condition
+ * field.
+ */
+ public function getUsers($restrictions = [])
+ {
+ $users = [];
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $users = DBManager::get()->fetchFirst("SELECT DISTINCT `user_id` " .
+ "FROM `" . $this->userDataDbTable . "` " .
+ "WHERE `" . $this->userDataDbField . "`" . $this->compareOperator .
+ "?", [$this->value]);
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ $sql = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` FROM `" . $this->userDataDbTable
+ . "`JOIN `user_inst` USING (`user_id`) ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "`" . $this->compareOperator
+ . ":value AND `user_inst`.`Institut_id` IN (:institutes) AND `user_inst`.`inst_perms` IN (:perms)";
+ $parameters = [
+ 'value' => $this->value,
+ 'institutes' => $allowed['allowed_institutes'],
+ 'perms' => ['autor', 'tutor', 'dozent', 'admin']
+ ];
+
+ $users = DBManager::get()->fetchFirst($sql.$where, $parameters);
+ }
+
+ return $users;
+ }
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailInstituteFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailInstituteFilter.php
new file mode 100644
index 0000000..4b4d49d
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailInstituteFilter.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace UserFilterFields\MassMail;
+
+use UserFilterField;
+use MassMail\MassMailPermission;
+use User;
+use DBManager;
+
+class MassMailInstituteFilter extends UserFilterField
+{
+ public $valuesDbTable = 'Institute';
+ public $valuesDbIdField = 'Institut_id';
+ public $valuesDbNameField = 'Name';
+ public $userDataDbTable = 'user_inst';
+ public $userDataDbField = 'Institut_id';
+
+ public static $sortOrder = 9;
+
+ public static function getTargets()
+ {
+ return ['employees'];
+ }
+
+ public function __construct($fieldId = '')
+ {
+ parent::__construct($fieldId);
+
+ $this->validCompareOperators = [
+ '=' => _('ist'),
+ '!=' => _('ist nicht'),
+ ];
+
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ // Get all available institutes from database, grouped by faculty.
+ $faculties = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name` FROM `Institute`
+ WHERE `fakultaets_id` = `Institut_id` ORDER BY `Name`"
+ );
+ foreach ($faculties as $f) {
+ $this->validValues[$f[$this->valuesDbIdField]] = $f[$this->valuesDbNameField];
+ $this->validValues[$f[$this->valuesDbIdField].'_children'] =
+ sprintf(_('%s und Untereinrichtungen'),
+ $f[$this->valuesDbNameField]);
+ $institutes = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name` FROM `Institute`
+ WHERE `fakultaets_id` = :fak AND `Institut_id` != :fak ORDER BY `Name`",
+ ['fak' => $f[$this->valuesDbIdField]]
+ );
+ foreach ($institutes as $i) {
+ $this->validValues[$i[$this->valuesDbIdField]] = $i[$this->valuesDbNameField];
+ }
+ }
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+ $this->validValues = [];
+
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ // Get all available institutes from database, grouped by faculty.
+ $faculties = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name` FROM `Institute`
+ WHERE `fakultaets_id` = `Institut_id` AND `Institut_id` IN (:allowed)
+ ORDER BY `Name`",
+ ['allowed' => $allowed['allowed_institutes']]
+ );
+ foreach ($faculties as $f) {
+ $this->validValues[$f[$this->valuesDbIdField]] = $f[$this->valuesDbNameField];
+ $this->validValues[$f[$this->valuesDbIdField] . '_children'] =
+ sprintf(_('%s und Untereinrichtungen'),
+ $f[$this->valuesDbNameField]);
+ $institutes = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name` FROM `Institute`
+ WHERE `fakultaets_id` = :fak AND `Institut_id` != :fak AND `Institut_id` IN (:allowed)
+ ORDER BY `Name`",
+ ['fak' => $f[$this->valuesDbIdField], 'allowed' => $allowed['allowed_institutes']]
+ );
+ foreach ($institutes as $i) {
+ $this->validValues[$i[$this->valuesDbIdField]] = $i[$this->valuesDbNameField];
+ }
+ }
+
+ $institutes = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name`
+ FROM `Institute`
+ WHERE `Institut_id` IN (:allowed)
+ AND `Institut_id` NOT IN (:processed)
+ ORDER BY `Name`",
+ [
+ 'allowed' => $allowed['allowed_institutes'],
+ 'processed' => count($this->validValues) > 0 ? array_keys($this->validValues) : ''
+ ]
+ );
+ foreach ($institutes as $i) {
+ $this->validValues[$i[$this->valuesDbIdField]] = $i[$this->valuesDbNameField];
+ }
+ }
+ }
+
+ public function getName()
+ {
+ return _('Einrichtung');
+ }
+
+ /**
+ * Gets all users belonging to a statusgroup with the given name. This is not done via statusgroup_id
+ * in ordner to enable several institutes as filter.
+ *
+ * @return array All users that are affected by the current condition
+ * field.
+ */
+ public function getUsers($restrictions = [])
+ {
+ $users = [];
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $users = DBManager::get()->fetchFirst(
+ "SELECT DISTINCT `user_id` " .
+ "FROM `" . $this->userDataDbTable . "` " .
+ "WHERE `" . $this->userDataDbField . "`" . $this->compareOperator .
+ ":value AND `inst_perms` IN (:perms)", ['value' => $this->value,
+ 'perms' => ['autor', 'tutor', 'dozent', 'admin']]
+ );
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ $sql = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` FROM `" . $this->userDataDbTable . "` ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "`" . $this->compareOperator
+ . ":value AND `Institut_id` IN (:institutes) AND `inst_perms` IN (:perms)";
+ $parameters = [
+ 'value' => $this->value,
+ 'institutes' => $allowed['allowed_institutes'],
+ 'perms' => ['autor', 'tutor', 'dozent', 'admin']
+ ];
+
+ $users = DBManager::get()->fetchFirst($sql.$where, $parameters);
+ }
+
+ return $users;
+ }
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailPermissionFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailPermissionFilter.php
new file mode 100644
index 0000000..eba3307
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailPermissionFilter.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * PermissionCondition.php
+ *
+ * All conditions concerning the semester of study in Stud.IP can be specified here.
+ *
+ * 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 Elmar Ludwig <elmar.ludwig@uos.de>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+namespace UserFilterFields\MassMail;
+
+use UserFilterFields\PermissionCondition;
+use User;
+use DBManager;
+use MassMail\MassMailPermission;
+
+class MassMailPermissionFilter extends PermissionCondition
+{
+
+ public string $target = '';
+
+ public static $sortOrder = 10;
+
+ /**
+ * @see \UserFilterField::getTargets()
+ */
+ public static function getTargets()
+ {
+ return ['employees'];
+ }
+
+ /**
+ * @see UserFilterField::__construct
+ */
+ public function __construct($fieldId = '')
+ {
+ $this->userDataDbTable = 'auth_user_md5';
+ $this->userDataDbField = 'perms';
+
+ parent::__construct($fieldId);
+
+ $this->validValues = [
+ 'autor' => _('Student/in'),
+ 'tutor' => _('Tutor/in'),
+ 'dozent' => _('Lehrende/r')
+ ];
+ }
+
+ /**
+ * Get this field's display name.
+ *
+ * @return String
+ */
+ public function getName()
+ {
+ return _('Globaler Status');
+ }
+
+ /**
+ * Gets all users with given gender.
+ *
+ * @return array All users that are affected by the current condition
+ * field.
+ */
+ public function getUsers($restrictions = array())
+ {
+ $users = [];
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $users = DBManager::get()->fetchFirst("SELECT DISTINCT `user_id` " .
+ "FROM `" . $this->userDataDbTable . "` " .
+ "WHERE `" . $this->userDataDbField . "`" . $this->compareOperator .
+ "?", [$this->value]);
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ $sql = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` FROM `" . $this->userDataDbTable . "` ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "`" . $this->compareOperator . ":value";
+ $parameters = ['value' => $this->value];
+
+ switch ($this->target) {
+ case 'employees':
+ $sql .= "JOIN `user_inst` USING (`user_id`) ";
+ $where .= " AND `user_inst`.`Institut_id` IN (:institutes) AND `user_inst`.`inst_perms` IN (:perms)";
+ $parameters['institutes'] = $allowed['allowed_institutes'];
+ $parameters['perms'] = ['autor', 'tutor', 'dozent', 'admin'];
+ break;
+ case 'students':
+ default:
+ $sql .= "JOIN `user_studiengang` USING (`user_id`) ";
+ $where .= " AND (
+ `user_studiengang`.`abschluss_id` IN (:degrees)
+ OR `user_studiengang`.`fach_id` IN (:subjects)
+ )";
+ $parameters['degrees'] = $allowed['allowed_degrees'];
+ $parameters['subjects'] = $allowed['allowed_subjects'];
+ }
+
+ $users = DBManager::get()->fetchFirst($sql.$where, $parameters);
+ }
+
+ return $users;
+ }
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailSelfAssignedInstituteFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailSelfAssignedInstituteFilter.php
new file mode 100644
index 0000000..8b732d2
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailSelfAssignedInstituteFilter.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace UserFilterFields\MassMail;
+
+use UserFilterField;
+use MassMail\MassMailPermission;
+use User;
+use DBManager;
+
+class MassMailSelfAssignedInstituteFilter extends UserFilterField
+{
+ public $valuesDbTable = 'Institute';
+ public $valuesDbIdField = 'Institut_id';
+ public $valuesDbNameField = 'Name';
+ public $userDataDbTable = 'user_inst';
+ public $userDataDbField = 'Institut_id';
+
+ public static $sortOrder = 9;
+
+ public static function getTargets()
+ {
+ return ['students'];
+ }
+
+ public function __construct($fieldId = '')
+ {
+ parent::__construct($fieldId);
+
+ $this->validCompareOperators = [
+ '=' => _('ist'),
+ '!=' => _('ist nicht'),
+ ];
+
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ // Get all available institutes from database, grouped by faculty.
+ $faculties = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name` FROM `Institute`
+ WHERE `fakultaets_id` = `Institut_id` ORDER BY `Name`"
+ );
+ foreach ($faculties as $f) {
+ $this->validValues[$f[$this->valuesDbIdField]] = $f[$this->valuesDbNameField];
+ $this->validValues[$f[$this->valuesDbIdField].'_children'] =
+ sprintf(_('%s und Untereinrichtungen'),
+ $f[$this->valuesDbNameField]);
+ $institutes = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name` FROM `Institute`
+ WHERE `fakultaets_id` = :fak AND `Institut_id` != :fak ORDER BY `Name`",
+ ['fak' => $f[$this->valuesDbIdField]]
+ );
+ foreach ($institutes as $i) {
+ $this->validValues[$i[$this->valuesDbIdField]] = $i[$this->valuesDbNameField];
+ }
+ }
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+ $this->validValues = [];
+
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ // Get all available institutes from database, grouped by faculty.
+ $faculties = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name` FROM `Institute`
+ WHERE `fakultaets_id` = `Institut_id` AND `Institut_id` IN (:allowed)
+ ORDER BY `Name`",
+ ['allowed' => $allowed['allowed_institutes']]
+ );
+ foreach ($faculties as $f) {
+ $this->validValues[$f[$this->valuesDbIdField]] = $f[$this->valuesDbNameField];
+ $this->validValues[$f[$this->valuesDbIdField] . '_children'] =
+ sprintf(_('%s und Untereinrichtungen'),
+ $f[$this->valuesDbNameField]);
+ $institutes = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name` FROM `Institute`
+ WHERE `fakultaets_id` = :fak AND `Institut_id` != :fak AND `Institut_id` IN (:allowed)
+ ORDER BY `Name`",
+ ['fak' => $f[$this->valuesDbIdField], 'allowed' => $allowed['allowed_institutes']]
+ );
+ foreach ($institutes as $i) {
+ $this->validValues[$i[$this->valuesDbIdField]] = $i[$this->valuesDbNameField];
+ }
+ }
+
+ $institutes = DBManager::get()->fetchAll(
+ "SELECT `Institut_id`, `Name`
+ FROM `Institute`
+ WHERE `Institut_id` IN (:allowed)
+ AND `Institut_id` NOT IN (:processed)
+ ORDER BY `Name`",
+ [
+ 'allowed' => $allowed['allowed_institutes'],
+ 'processed' => count($this->validValues) > 0 ? array_keys($this->validValues) : ''
+ ]
+ );
+ foreach ($institutes as $i) {
+ $this->validValues[$i[$this->valuesDbIdField]] = $i[$this->valuesDbNameField];
+ }
+ }
+ }
+
+ public function getName()
+ {
+ return _('Selbst zugeordnete Einrichtung');
+ }
+
+ /**
+ * Gets all users belonging to a statusgroup with the given name. This is not done via statusgroup_id
+ * in ordner to enable several institutes as filter.
+ *
+ * @return array All users that are affected by the current condition
+ * field.
+ */
+ public function getUsers($restrictions = [])
+ {
+ $users = [];
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $users = DBManager::get()->fetchFirst("SELECT DISTINCT `user_id` " .
+ "FROM `" . $this->userDataDbTable . "` " .
+ "WHERE `" . $this->userDataDbField . "`" . $this->compareOperator .
+ "? AND `inst_perms` = 'user'", [$this->value]);
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ $sql = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` FROM `" . $this->userDataDbTable
+ . "`JOIN `user_inst` USING (`user_id`) ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "`" . $this->compareOperator
+ . ":value AND `user_inst`.`Institut_id` IN (:institutes) AND `user_inst`.`inst_perms` = 'user'";
+ $parameters = [
+ 'value' => $this->value,
+ 'institutes' => $allowed['institutes']->pluck('id')
+ ];
+
+ $users = DBManager::get()->fetchFirst($sql.$where, $parameters);
+ }
+
+ return $users;
+ }
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailSemesterOfStudyFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailSemesterOfStudyFilter.php
new file mode 100644
index 0000000..f92c220
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailSemesterOfStudyFilter.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace UserFilterFields\MassMail;
+
+use UserFilterFields\SemesterOfStudyCondition;
+
+class MassMailSemesterOfStudyFilter extends SemesterOfStudyCondition
+{
+ // --- ATTRIBUTES ---
+ public $valuesDbTable = 'user_studiengang';
+ public $valuesDbIdField = 'semester';
+ public $userDataDbTable = 'user_studiengang';
+ public $userDataDbField = 'semester';
+
+ /**
+ * @see \UserFilterField::getTargets()
+ */
+ public static function getTargets()
+ {
+ return ['students'];
+ }
+
+ /**
+ * @see UserFilterField::__construct
+ */
+ public function __construct($fieldId='')
+ {
+ parent::__construct($fieldId);
+ $this->relations = [
+ 'MassMailDegreeFilter' => [
+ 'local_field' => 'abschluss_id',
+ 'foreign_field' => 'abschluss_id'
+ ],
+ 'MassMailSubjectFilter' => [
+ 'local_field' => 'fach_id',
+ 'foreign_field' => 'fach_id'
+ ]
+ ];
+ $this->validCompareOperators = [
+ '>=' => _('mindestens'),
+ '<=' => _('höchstens'),
+ '=' => _('ist'),
+ '!=' => _('ist nicht')
+ ];
+ if (isset(self::$cached_valid_values[static::class])) {
+ $this->validValues = self::$cached_valid_values[static::class];
+ } else {
+ // Initialize to some value in case there are no semester numbers.
+ $maxsem = 15;
+ // Calculate the maximal available semester.
+ $stmt = \DBManager::get()->query("SELECT MAX(" . $this->valuesDbIdField . ") AS maxsem " .
+ "FROM `" . $this->valuesDbTable . "`");
+ if ($current = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($current['maxsem']) {
+ $maxsem = $current['maxsem'];
+ }
+ }
+ for ($i = 1; $i <= $maxsem; $i++) {
+ $this->validValues[$i] = $i;
+ }
+ self::$cached_valid_values[static::class] = $this->validValues;
+ }
+ }
+
+ /**
+ * Get this field's display name.
+ *
+ * @return String
+ */
+ public function getName()
+ {
+ return _('Fachsemester');
+ }
+
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailStatusgroupFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailStatusgroupFilter.php
new file mode 100644
index 0000000..61a04be
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailStatusgroupFilter.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace UserFilterFields\MassMail;
+
+use UserFilterField;
+use MassMail\MassMailPermission;
+use User;
+use DBManager;
+
+class MassMailStatusgroupFilter extends UserFilterField
+{
+ public $valuesDbTable = 'statusgruppen';
+ public $valuesDbIdField = 'statusgruppe_id';
+ public $valuesDbNameField = 'name';
+ public $userDataDbTable = 'statusgruppe_user';
+ public $userDataDbField = 'statusgruppe_id';
+
+ public static $sortOrder = 10;
+
+ public static function getTargets()
+ {
+ return ['employees'];
+ }
+
+ public function __construct($fieldId = '')
+ {
+ parent::__construct($fieldId);
+
+ $this->validCompareOperators = [
+ '=' => _('ist'),
+ '!=' => _('ist nicht'),
+ ];
+
+ $this->validValues = [];
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $this->validValues = DBManager::get()->fetchFirst(
+ "SELECT DISTINCT `name` FROM `statusgruppen` ORDER BY `name` ASC"
+ );
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ $this->validValues = DBManager::get()->fetchFirst(
+ "SELECT DISTINCT `name` FROM `statusgruppen` WHERE `range_id` IN (:institutes) ORDER BY `name` ASC",
+ ['institutes' => $allowed['allowed_institutes']]
+ );
+ }
+ }
+
+ public function getName()
+ {
+ return _('Statusgruppe');
+ }
+
+ /**
+ * Gets all users belonging to a statusgroup with the given name. This is not done via statusgroup_id
+ * in ordner to enable several institutes as filter.
+ *
+ * @return array All users that are affected by the current condition
+ * field.
+ */
+ public function getUsers($restrictions = [])
+ {
+ $users = [];
+ if (MassMailPermission::has(User::findCurrent()->id, true)) {
+ $users = DBManager::get()->fetchFirst("SELECT DISTINCT `user_id` " .
+ "FROM `" . $this->userDataDbTable . "` " .
+ "WHERE `" . $this->userDataDbField . "`" . $this->compareOperator .
+ "?", [$this->value]);
+ } else if (MassMailPermission::has(User::findCurrent()->id)) {
+
+ $allowed = MassMailPermission::getForUser(User::findCurrent());
+
+ $sql = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` FROM `" . $this->userDataDbTable
+ . "`JOIN `user_inst` USING (`user_id`) ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "`" . $this->compareOperator
+ . ":value AND `user_inst`.`Institut_id` IN (:institutes) AND `user_inst`.`inst_perms` IN (:perms)";
+ $parameters = [
+ 'value' => $this->value,
+ 'institutes' => $allowed['allowed_institutes'],
+ 'perms' => ['autor', 'tutor', 'dozent', 'admin']
+ ];
+
+ $users = DBManager::get()->fetchFirst($sql.$where, $parameters);
+ }
+
+ return $users;
+ }
+}
diff --git a/lib/classes/UserFilterFields/MassMail/MassMailSubjectFilter.php b/lib/classes/UserFilterFields/MassMail/MassMailSubjectFilter.php
new file mode 100644
index 0000000..977b277
--- /dev/null
+++ b/lib/classes/UserFilterFields/MassMail/MassMailSubjectFilter.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace UserFilterFields\MassMail;
+
+use UserFilterFields\SubjectCondition;
+
+class MassMailSubjectFilter extends SubjectCondition
+{
+ /**
+ * @see \UserFilterField::getTargets()
+ */
+ public static function getTargets()
+ {
+ return ['students'];
+ }
+
+ public function __construct($fieldId = '')
+ {
+ parent::__construct($fieldId);
+
+ if (!\MassMail\MassMailPermission::has(\User::findCurrent()->id, true)) {
+ $this->validValues = [];
+
+ $permission = \MassMail\MassMailPermission::getForUser(\User::findCurrent(), true);
+
+ foreach ($permission['allowed_subjects'] as [$id, $name]) {
+ $this->validValues[$id] = (string) $name;
+ }
+ }
+ }
+
+ public function getUsers($restrictions = [])
+ {
+ $users = [];
+
+ if (\MassMail\MassMailPermission::has(\User::findCurrent()->id, true)) {
+ $users = parent::getUsers($restrictions);
+ } else if (count($this->validValues) > 0) {
+ // Standard query getting the values without respecting other values.
+ $select = "SELECT DISTINCT `" . $this->userDataDbTable . "`.`user_id` ";
+ $from = "FROM `" . $this->userDataDbTable . "` ";
+ $where = "WHERE `" . $this->userDataDbTable . "`.`" . $this->userDataDbField .
+ "`" . $this->compareOperator . "?";
+ $parameters = [$this->value];
+ $joinedTables = [
+ $this->userDataDbTable => true
+ ];
+ // Check if there are restrictions given.
+ foreach ($restrictions as $otherField => $restriction) {
+ // We only take the value into consideration if it represents a valid restriction.
+ if ($this->relations[$otherField]) {
+ // Do we need to join in another table?
+ if (!$joinedTables[$restriction['table']]) {
+ $joinedTables[$restriction['table']] = true;
+ $from .= " INNER JOIN `" . $restriction['table'] . "` ON (`" .
+ $this->userDataDbTable . "`.`" .
+ $this->relations[$otherField]['local_field'] . "`=`" .
+ $restriction['table'] . "`.`" .
+ $this->relations[$otherField]['foreign_field'] . "`)";
+ }
+ // Expand WHERE statement with the value from restriction.
+ $where .= " AND `" . $restriction['table'] . "`.`" .
+ $restriction['field'] . "`" . $restriction['compare'] . "?";
+ $parameters[] = $restriction['value'];
+ }
+ }
+
+ $where .= " AND `" . $this->userDataDbTable . "`.`" . $this->userDataDbField . "` IN (?)";
+ $parameters[] = array_keys($this->validValues);
+
+ // Get all the users that fulfill the condition.
+ $users = \DBManager::get()->fetchFirst($select . $from . $where, $parameters);
+ }
+
+ return $users;
+ }
+
+ /**
+ * Gets the value for the given user that is relevant for this
+ * condition field. Here, this method looks up the study degree(s)
+ * for the user. These can then be compared with the required degrees
+ * whether they fit.
+ *
+ * @param String $userId User to check.
+ * @param array $additional conditions that are required for check.
+ * @return array The value(s) for this user.
+ */
+ public function getUserValues($userId, $additional = null)
+ {
+ if (\MassMail\MassMailPermission::has(\User::findCurrent()->id, true)) {
+ $result = parent::getUserValues($userId, $additional);
+ } else {
+ $result = [];
+ $query = "SELECT DISTINCT `" . $this->userDataDbField . "` " .
+ "FROM `" . $this->userDataDbTable . "` " .
+ "WHERE `user_id`=?";
+ $parameters = [$userId];
+ // Additional requirements given...
+ if (is_array($additional)) {
+
+ // Don't use the same database field twice as this can only get ugly.
+ $usedFields = [$this->userDataDbField];
+
+ foreach ($additional as $a_condition) {
+ if (
+ $a_condition->id != $this->id
+ && $this->userDataDbTable === $a_condition->userDataDbTable
+ && !in_array($a_condition->userDataDbField, $usedFields)
+ ) {
+ $query .= " AND `" . $a_condition->userDataDbField . "` " . $a_condition->compareOperator . "?";
+ $parameters[] = $a_condition->value;
+ }
+ }
+ }
+ // Get semester of study for user.
+ $stmt = \DBManager::get()->prepare($query);
+ $stmt->execute($parameters);
+ while ($current = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $current[$this->userDataDbField];
+ }
+ }
+ return $result;
+ }
+
+}
diff --git a/lib/classes/admission/userfilter/PermissionCondition.php b/lib/classes/UserFilterFields/PermissionCondition.php
index fe9458c..10212c7 100644
--- a/lib/classes/admission/userfilter/PermissionCondition.php
+++ b/lib/classes/UserFilterFields/PermissionCondition.php
@@ -1,4 +1,5 @@
<?php
+
/**
* PermissionCondition.php
*
@@ -13,9 +14,11 @@
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
-class PermissionCondition extends UserFilterField
+namespace UserFilterFields;
+
+class PermissionCondition extends \UserFilterField
{
- public $sortOrder = 7;
+ public static $sortOrder = 7;
/**
* @see UserFilterField::__construct
diff --git a/lib/classes/admission/userfilter/SemesterOfStudyCondition.php b/lib/classes/UserFilterFields/SemesterOfStudyCondition.php
index 5794f75..f66789f 100644
--- a/lib/classes/admission/userfilter/SemesterOfStudyCondition.php
+++ b/lib/classes/UserFilterFields/SemesterOfStudyCondition.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SemesterOfStudyCondition.php
*
@@ -13,7 +14,9 @@
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
-class SemesterOfStudyCondition extends UserFilterField
+namespace UserFilterFields;
+
+class SemesterOfStudyCondition extends \UserFilterField
{
// --- ATTRIBUTES ---
public $valuesDbTable = 'user_studiengang';
@@ -21,7 +24,7 @@ class SemesterOfStudyCondition extends UserFilterField
public $userDataDbTable = 'user_studiengang';
public $userDataDbField = 'semester';
- public $sortOrder = 4;
+ public static $sortOrder = 4;
// --- OPERATIONS ---
@@ -54,9 +57,9 @@ class SemesterOfStudyCondition extends UserFilterField
// Initialize to some value in case there are no semester numbers.
$maxsem = 15;
// Calculate the maximal available semester.
- $stmt = DBManager::get()->query("SELECT MAX(" . $this->valuesDbIdField . ") AS maxsem " .
+ $stmt = \DBManager::get()->query("SELECT MAX(" . $this->valuesDbIdField . ") AS maxsem " .
"FROM `" . $this->valuesDbTable . "`");
- if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ if ($current = $stmt->fetch(\PDO::FETCH_ASSOC)) {
if ($current['maxsem']) {
$maxsem = $current['maxsem'];
}
diff --git a/lib/classes/admission/userfilter/StgteilVersionCondition.php b/lib/classes/UserFilterFields/StgteilVersionCondition.php
index ec5c1f3..59bb035 100644
--- a/lib/classes/admission/userfilter/StgteilVersionCondition.php
+++ b/lib/classes/UserFilterFields/StgteilVersionCondition.php
@@ -1,4 +1,5 @@
<?php
+
/**
* StgteilVersionCondition.php
*
@@ -13,7 +14,9 @@
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
-class StgteilVersionCondition extends UserFilterField
+namespace UserFilterFields;
+
+class StgteilVersionCondition extends \UserFilterField
{
// --- ATTRIBUTES ---
public $valuesDbTable = 'mvv_stgteilversion';
@@ -22,14 +25,14 @@ class StgteilVersionCondition extends UserFilterField
public $userDataDbTable = 'user_studiengang';
public $userDataDbField = 'version_id';
- public $sortOrder = 5;
+ public static $sortOrder = 5;
public static $isParameterized = true;
public static function getParameterizedTypes()
{
- if (Config::get()->DISPLAY_STGTEILVERSION_USERFILTER) {
- $filter = new StgteilVersionCondition;
+ if (\Config::get()->DISPLAY_STGTEILVERSION_USERFILTER) {
+ $filter = new StgteilVersionCondition();
$fields['StgteilVersionCondition'] = $filter->getName();
return $fields;
} else {
@@ -48,13 +51,13 @@ class StgteilVersionCondition extends UserFilterField
];
if ($this->valuesDbNameField) {
// Get all available values from database.
- $stmt = DBManager::get()->query(
+ $stmt = \DBManager::get()->query(
"SELECT DISTINCT `version_id`, `fach`.`name` ".
"FROM `mvv_stgteilversion` LEFT JOIN mvv_stgteil USING (stgteil_id)".
"LEFT JOIN fach USING (fach_id)".
"WHERE `mvv_stgteilversion`.`stat` = 'genehmigt' ORDER BY `fach`.`name` ASC");
- while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ while ($current = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$this->validValues[$current[$this->valuesDbIdField]] = $current[$this->valuesDbNameField];
}
}
@@ -66,7 +69,7 @@ class StgteilVersionCondition extends UserFilterField
}
foreach ($this->validValues as $version_id => $name) {
- $stgteilversion = StgteilVersion::find($version_id);
+ $stgteilversion = \StgteilVersion::find($version_id);
$this->validValues[$version_id] = $stgteilversion->getDisplayName();
}
}
diff --git a/lib/classes/admission/userfilter/SubjectCondition.php b/lib/classes/UserFilterFields/SubjectCondition.php
index 7aa5f26..e9ac1a0 100644
--- a/lib/classes/admission/userfilter/SubjectCondition.php
+++ b/lib/classes/UserFilterFields/SubjectCondition.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SubjectCondition.php
*
@@ -13,7 +14,9 @@
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
-class SubjectCondition extends UserFilterField
+namespace UserFilterFields;
+
+class SubjectCondition extends \UserFilterField
{
// --- ATTRIBUTES ---
public $valuesDbTable = 'fach';
@@ -22,7 +25,7 @@ class SubjectCondition extends UserFilterField
public $userDataDbTable = 'user_studiengang';
public $userDataDbField = 'fach_id';
- public $sortOrder = 2;
+ public static $sortOrder = 2;
// --- OPERATIONS ---
diff --git a/lib/classes/admission/userfilter/SubjectConditionAny.php b/lib/classes/UserFilterFields/SubjectConditionAny.php
index 3a3712b..c99bcb8 100644
--- a/lib/classes/admission/userfilter/SubjectConditionAny.php
+++ b/lib/classes/UserFilterFields/SubjectConditionAny.php
@@ -14,15 +14,15 @@
* @category Stud.IP
*/
-require_once realpath(__DIR__ . '/..') . '/UserFilterField.php';
+namespace UserFilterFields;
-class SubjectConditionAny extends UserFilterField
+class SubjectConditionAny extends \UserFilterField
{
// --- ATTRIBUTES ---
public $userDataDbTable = 'user_studiengang';
public $userDataDbField = 'fach_id';
- public $sortOrder = 3;
+ public static $sortOrder = 3;
// --- OPERATIONS ---
diff --git a/lib/classes/UserFilterRange.php b/lib/classes/UserFilterRange.php
new file mode 100644
index 0000000..a5a53e1
--- /dev/null
+++ b/lib/classes/UserFilterRange.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * UserFilterRange.php
+ *
+ * An interface that provides information about necessary permissions for editing a UserFilter object.
+ *
+ * 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 Thomas Hackl <thomas.hackl@uni-passau.de>
+ * @since 6.0
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ */
+
+interface UserFilterRange {
+
+ /**
+ * Check whether the given user can edit the given UserFilter object.
+ * @param User $user
+ * @param UserFilter $filter
+ * @return bool
+ */
+ public function canEditFilter(User $user, UserFilter $filter): bool;
+
+}
diff --git a/lib/classes/admission/CourseSet.php b/lib/classes/admission/CourseSet.php
index e7f3dee..d93cfb0 100644
--- a/lib/classes/admission/CourseSet.php
+++ b/lib/classes/admission/CourseSet.php
@@ -15,7 +15,7 @@
* @category Stud.IP
*/
-class CourseSet
+class CourseSet implements UserFilterRange
{
// --- ATTRIBUTES ---
@@ -969,6 +969,7 @@ class CourseSet
}
// Store all rules.
foreach ($this->admissionRules as $rule) {
+ $rule->courseSetId = $this->id;
// Store each rule...
$rule->store();
// ... and its connection to the current course set.
@@ -1194,4 +1195,47 @@ class CourseSet
$this->admissionRules = $cloned_rules;
}
+ /**
+ * @see UserFilterRange::canEdit()
+ */
+ public function canEditFilter(User $user, UserFilter $filter): bool
+ {
+ if ($GLOBALS['perm']->have_perm('root', $user->id)) {
+ return true;
+ }
+
+ // Check general permissions on course set creation/editing.
+ $permission = $GLOBALS['perm']->have_perm('admin', $user->id)
+ || (
+ Config::get()->ALLOW_DOZENT_COURSESET_ADMIN
+ && $GLOBALS['perm']->have_perm('dozent', $user->id)
+ );
+
+ // Get all rules where filter can be present.
+ $ruleTypes = array_filter(
+ $this->getAdmissionRules(),
+ fn($rule) => in_array(get_class($rule), [ConditionalAdmission::class, PreferentialAdmission::class])
+ );
+
+ // Get my institute's IDs.
+ $institutes = array_map(
+ fn ($i) => $i['Institut_id'],
+ Institute::getMyInstitutes($user->id)
+ );
+ $matchingInstitutes = array_intersect(array_keys($this->institutes), $institutes);
+
+ /*
+ * Check whether:
+ * - this course set has rules than can have UserFilter objects
+ * - the given user is allowed to create/edit course sets at all
+ * - this course set belongs to the given user or is not private and belongs to one of this user's institutes
+ */
+ return $permission
+ && count($ruleTypes) > 0
+ && (
+ $this->user_id === $user->id
+ || !$this->private && count($matchingInstitutes) > 0
+ );
+ }
+
} /* end of class CourseSet */
diff --git a/lib/classes/forms/CheckboxCollectionInput.php b/lib/classes/forms/CheckboxCollectionInput.php
new file mode 100644
index 0000000..7859b9a
--- /dev/null
+++ b/lib/classes/forms/CheckboxCollectionInput.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Studip\Forms;
+
+class CheckboxCollectionInput extends Input
+{
+ public function render()
+ {
+ $template = $GLOBALS['template_factory']->open('forms/checkbox_collection_input');
+ $template->title = $this->title;
+ $template->name = $this->name;
+ $template->selected = $this->value;
+ $template->required = $this->required;
+
+ $template->collapsable = $this->attributes['collapsable'] ?? false;
+ if (isset($this->attributes['collapsable'])) {
+ unset($this->attributes['collapsable']);
+ }
+ $options = $this->extractOptionsFromAttributes($this->attributes);
+
+ $template->attributes = arrayToHtmlAttributes($this->attributes);
+ $template->options = $options;
+ return $template->render();
+ }
+}
diff --git a/lib/classes/forms/Fieldset.php b/lib/classes/forms/Fieldset.php
index e7bced0..d1915bd 100644
--- a/lib/classes/forms/Fieldset.php
+++ b/lib/classes/forms/Fieldset.php
@@ -5,6 +5,8 @@ namespace Studip\Forms;
class Fieldset extends Part
{
protected $legend = null;
+ protected bool $collapsable = false;
+ protected bool $collapsed = false;
public function __construct($legend = null)
{
@@ -16,10 +18,25 @@ class Fieldset extends Part
$this->legend = $legend;
}
+
+ public function setCollapsable(bool $state = true): Fieldset
+ {
+ $this->collapsable = $state;
+ return $this;
+ }
+
+ public function setCollapsed(bool $state = true): Fieldset
+ {
+ $this->collapsed = $state;
+ return $this;
+ }
+
public function render()
{
$template = $GLOBALS['template_factory']->open('forms/fieldset');
$template->legend = $this->legend;
+ $template->collapsable = $this->collapsable;
+ $template->collapsed = $this->collapsable && $this->collapsed;
$template->parts = $this->parts;
return $template->render();
}
diff --git a/lib/classes/forms/FileInput.php b/lib/classes/forms/FileInput.php
new file mode 100644
index 0000000..5862b3c
--- /dev/null
+++ b/lib/classes/forms/FileInput.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Studip\Forms;
+
+class FileInput extends Input
+{
+
+ public function render()
+ {
+ $template = $GLOBALS['template_factory']->open('forms/file_input');
+ $template->title = $this->title;
+ $template->name = $this->name;
+ $template->folder = $this->value;
+ $template->id = md5(uniqid());
+ $template->uploadUrl = $this->attributes['upload_url'];
+ $template->multiple = $this->attributes['multiple'] ?? '';
+ $template->accept = $this->attributes['accept'] ?? '*/*';
+ $template->required = $this->attributes['required'] ?? '';
+
+ return $template->render();
+ }
+
+}
diff --git a/lib/classes/forms/Form.php b/lib/classes/forms/Form.php
index 0148c9c..f9b27cd 100644
--- a/lib/classes/forms/Form.php
+++ b/lib/classes/forms/Form.php
@@ -151,7 +151,9 @@ class Form extends Part
//Now initializing the fieldset:
$fieldset = new Fieldset($params['legend'] ?: _("Daten"));
- $fieldset->setContextObject($object);
+ $fieldset->setContextObject($object)
+ ->setCollapsable($params['collapsable'] ?? false)
+ ->setCollapsed($params['collapsed'] ?? false);
$this->addPart($fieldset);
foreach ($fields as $fieldname => $fielddata) {
@@ -578,4 +580,20 @@ class Form extends Part
}
return $value;
}
+
+ /**
+ * Checks whether this form has a file input and thus needs its enctype set.
+ * @return bool
+ */
+ public function hasFileInput()
+ {
+ foreach ($this->getAllInputs() as $input) {
+ if (get_class($input) === FileInput::class) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
}
diff --git a/lib/classes/forms/QuicksearchListInput.php b/lib/classes/forms/QuicksearchListInput.php
new file mode 100644
index 0000000..3cbc29a
--- /dev/null
+++ b/lib/classes/forms/QuicksearchListInput.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Studip\Forms;
+
+class QuicksearchListInput extends Input
+{
+ public function render()
+ {
+ $template = $GLOBALS['template_factory']->open('forms/quicksearchlist_input');
+ $template->title = $this->title;
+ $template->name = $this->name;
+ $template->value = $this->value;
+ $template->id = md5(uniqid());
+ $template->required = $this->required;
+ $template->attributes = arrayToHtmlAttributes($this->attributes);
+
+ return $template->render();
+ }
+}
diff --git a/lib/classes/forms/SerialWysiwygInput.php b/lib/classes/forms/SerialWysiwygInput.php
new file mode 100644
index 0000000..3d4551f
--- /dev/null
+++ b/lib/classes/forms/SerialWysiwygInput.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Studip\Forms;
+
+use MassMail\MassMailMarker;
+
+class SerialWysiwygInput extends WysiwygInput
+{
+
+ public function render()
+ {
+ if (!isset($this->attributes['id'])) {
+ $id = md5(uniqid());
+ $this->attributes['id'] = $id;
+ } else {
+ $id = $this->attributes['id'];
+ }
+
+ $template = $GLOBALS['template_factory']->open('forms/serial_wysiwyg_input');
+ $template->title = $this->title;
+ $template->name = $this->name;
+ $template->value = $this->value;
+ $template->id = $id;
+ $template->required = $this->required;
+ $template->markers = $this->attributes['markers'];
+ $template->attributes = $this->attributes;
+ return $template->render();
+ }
+
+ public function getRequestValue()
+ {
+ return \Studip\Markup::purifyHtml(\Request::get($this->name));
+ }
+}
diff --git a/lib/classes/forms/UserFilterInput.php b/lib/classes/forms/UserFilterInput.php
new file mode 100644
index 0000000..0b415b7
--- /dev/null
+++ b/lib/classes/forms/UserFilterInput.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Studip\Forms;
+
+/**
+ * The Text class represents a part of a form that displays a user filter selection.
+ */
+class UserFilterInput extends Input
+{
+
+ public function getValue()
+ {
+ $value = [];
+ foreach ($this->getContextObject()->filters as $connection) {
+ $filter = $connection->userfilter;
+ $one = [
+ 'id' => $filter->getId(),
+ 'attributes' => [
+ 'text' => $filter->toString(),
+ 'fields' => []
+ ]
+ ];
+ foreach ($filter->getFields() as $field) {
+ $one['attributes']['fields'][] = [
+ 'id' => $field->getId(),
+ 'attributes' => [
+ 'type' => get_class($field),
+ 'compare-operator' => $field->getCompareOperator(),
+ 'value' => $field->getValue()
+ ]
+ ];
+ }
+ $value[] = $one;
+ }
+ return json_encode($value);
+ }
+
+ public function getRequestValue()
+ {
+ return json_decode(\Request::get($this->name), true);
+ }
+
+ public function hasValidation(): bool
+ {
+ return false;
+ }
+
+ public function render(): string
+ {
+ $template = $GLOBALS['template_factory']->open('forms/user_filter_input');
+ $template->title = $this->title;
+ $template->name = $this->name;
+ $template->value = $this->value;
+ $template->id = md5(uniqid());
+ $template->required = $this->required;
+ $template->attributes = arrayToHtmlAttributes($this->attributes);
+ return $template->render();
+ }
+
+}
diff --git a/lib/cronjobs/send_massmails.php b/lib/cronjobs/send_massmails.php
new file mode 100644
index 0000000..f712f60
--- /dev/null
+++ b/lib/cronjobs/send_massmails.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * send_massmails.php
+ *
+ * @author Thomas Hackl <hackl@data-quest.de>
+ * @access public
+ * @since 6.0
+ */
+
+/**
+ * Cronjob class to send massmails.
+ */
+class SendMassmailsJob extends CronJob
+{
+
+ /**
+ * Returns the name of the cronjob.
+ * @return string : name of the cronjob
+ */
+ public static function getName()
+ {
+ return _('Nachrichten an Zielgruppen senden');
+ }
+
+ /**
+ * Returns the description of the cronjob.
+ * @return string : description of the cronjob.
+ */
+ public static function getDescription()
+ {
+ return _('Sendet alle anstehenden Nachrichten an Zielgruppen und räumt bereits gesendete auf.');
+ }
+
+ /**
+ * Sends all mass mails.
+ * @param integer $last_result : not evaluated for execution, so any integer
+ * will do. Usually it would be a unix-timestamp of last execution. But in
+ * this case we don't care at all.
+ * @param array $parameters : not needed here
+ */
+ public function execute($last_result, $parameters = [])
+ {
+ // Find all messages that need to be sent:
+ foreach (\MassMail\MassMailMessage::findUnsent() as $message) {
+ // Mark message as "currently working on".
+ $message->locked = 1;
+ $message->store();
+
+ $messaging = new messaging();
+
+ // Markers present: this must be a personalized message to every recipient.
+ if ($message->hasMarkers()) {
+
+ foreach ($message->getRecipients() as $recipient) {
+
+ $mail = new Message();
+ $mail->setId($mail->getNewId());
+
+ $result = $messaging->insert_message(
+ $message->replaceMarkers(User::findOneByUsername($recipient)),
+ $recipient,
+ $message->sender_id,
+ time(),
+ $mail->id,
+ '',
+ '',
+ $message->subject
+ );
+
+ echo sprintf("Sending message %s to %s\n", $message->subject, $recipient);
+ }
+
+ // No markers -> we can send this as one single message to everyone at once.
+ } else {
+
+ $mail = new Message();
+ $mail->setId($mail->getNewId());
+
+ $result = $messaging->insert_message(
+ $message->message,
+ $message->getRecipients(),
+ $message->sender_id,
+ time(),
+ $mail->id,
+ '',
+ '',
+ $message->subject
+ );
+
+ echo sprintf("Sending message %s to %u recipients\n", $message->subject, count($message->getRecipients()));
+ }
+
+ if ($result) {
+ echo "Success!\n";
+ $message->locked = 0;
+ $message->sent = 1;
+ $message->store();
+ }
+
+ }
+
+ // Now cleanup all messages that have been sent and are older than the configured number of days.
+ foreach (\MassMail\MassMailMessage::findObsolete() as $message) {
+ $message->delete();
+ }
+ }
+}
diff --git a/lib/models/MassMail/MassMailFilter.php b/lib/models/MassMail/MassMailFilter.php
new file mode 100644
index 0000000..8d88c65
--- /dev/null
+++ b/lib/models/MassMail/MassMailFilter.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace MassMail;
+
+class MassMailFilter extends \SimpleORMap
+{
+
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'massmail_filter';
+
+ $config['additional_fields']['userfilter']['get'] = function ($entry) {
+ return new \UserFilter($entry->filter_id);
+ };
+ $config['registered_callbacks']['before_delete'][] = 'cbDeleteUserFilter';
+ $config['registered_callbacks']['after_store'][] = 'cbUpdateUserFilterRange';
+
+ parent::configure($config);
+ }
+
+ public function cbDeleteUserFilter()
+ {
+ $filter = new \UserFilter($this->filter_id);
+ $filter->delete();
+ }
+
+ public function cbUpdateUserFilterRange()
+ {
+ $filter = new \UserFilter($this->filter_id);
+ $filter->setRange(MassMailMessage::class, $this->message_id);
+ $filter->store();
+ }
+
+}
diff --git a/lib/models/MassMail/MassMailMarker.php b/lib/models/MassMail/MassMailMarker.php
new file mode 100644
index 0000000..d3dca6e
--- /dev/null
+++ b/lib/models/MassMail/MassMailMarker.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace MassMail;
+
+use \User, \DBManager, \StudipPDO, \PDO;
+
+class MassMailMarker extends \SimpleORMap
+{
+
+ /*
+ * This seems to be necessary because of the direct reference in getDescription.
+ * Otherwise a PHP warning is thrown.
+ */
+ public string $description;
+
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'massmail_markers';
+
+ parent::configure($config);
+ }
+
+ public static function findAll($root = false) {
+ return $root
+ ? static::findBySQL("1 ORDER BY `position`")
+ : static::findbyRoot_only("0 ORDER BY `position`");
+ }
+
+ /**
+ * Replaces markers contained in the given text with their replacement value for the given user.
+ *
+ * @param string $text
+ * @param User $user
+ * @param MassMailMarker[] $markers
+ * @return string|string[]
+ */
+ public static function processText(string $text, User $user, array $markers) {
+ $find = [];
+ $replace = [];
+ foreach ($markers as $marker) {
+ if ((!$marker->root_only || MassMailPermission::has(User::findCurrent()->id, true))
+ && strpos($text, '{{' . $marker->marker . '}}') !== false
+ && $marker->type != 'token') {
+ $find[] = '{{' . $marker->marker . '}}';
+ $replace[] = $marker->replaceMarker($user);
+ }
+ }
+ $text = str_replace($find, $replace, $text);
+ return $text;
+ }
+
+ /**
+ * Replaces tokens in the given text with a token for the given user.
+ * @param int $message_id
+ * @param string $text
+ * @param User $user
+ * @return string|string[]
+ */
+ public static function processToken(int $message_id, string $text, User $user)
+ {
+ foreach (self::findByType('token') as $marker) {
+ if ((!$marker->root_only || MassMailPermission::has(User::findCurrent()->id, true)) &&
+ strpos($text, '{{' . $marker->marker . '}}') !== false) {
+ $text = str_replace('{{' . $marker->marker . '}}',
+ $marker->getReplacementToken($message_id, $user),
+ $text
+ );
+ }
+ }
+ return $text;
+ }
+
+ /**
+ * This is a helper get function which gets the translated marker description. As the regular i18 mechanism for
+ * translateable content is not working here (thie is just shown in the GUI but stored dynamically in the database)
+ * I really do not know how to do that otherwise.
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return _($this->description);
+ }
+
+ /**
+ * Replaces the current marker text according to the given user.
+ * @param User $user
+ * @return array|mixed|\SimpleORMapCollection|string|string[]|void|null
+ */
+ public function replaceMarker(User $user)
+ {
+ $replacement = $this->replacement;
+
+ switch ($user->geschlecht) {
+ case 2:
+ if ($this->replacement_female) {
+ $replacement = $this->replacement_female;
+ }
+ break;
+ case 0:
+ case 3:
+ if ($this->replacement_unknown) {
+ $replacement = $this->replacement_unknown;
+ }
+ break;
+ }
+
+ switch ($this->type) {
+ // Just plain text replacing the marker, just check if other markers are included here.
+ case 'text':
+ if (strpos($replacement, '{{') !== false) {
+ $matches = [];
+ preg_match_all('/{{([a-zA-Z0-9\-_]+)}}/m', $replacement, $matches);
+ foreach ($matches[1] as $match) {
+ $replacement = str_replace('{' . $match . '}',
+ MassMailMarker::findOneByMarker(trim($match))->replaceMarker($user),
+ $replacement
+ );
+ }
+ }
+ return $replacement;
+
+ // Content from one or more database columns replaces the marker.
+ case 'database':
+ $data = words($replacement);
+ $find = [];
+ $replace = [];
+ foreach ($data as $entry) {
+ if (strpos($entry, '{') !== false) {
+ $matches = [];
+ preg_match_all('/{{([a-zA-Z0-9\-_]+)}}/m', $entry, $matches);
+ foreach ($matches[1] as $match) {
+ $replacement = str_replace($entry,
+ MassMailMarker::findOneByMarker(trim($match))->replaceMarker($user),
+ $replacement
+ );
+ }
+ } else {
+ // Extract the database fields...
+ [$table, $column] = explode('.', $entry);
+ // ... and query database for values to insert.
+ $stmt = DBManager::get()->prepare("SELECT `:column`
+ FROM `:table` WHERE `user_id` = :userid LIMIT 1");
+ $stmt->bindParam('column', $column, StudipPDO::PARAM_COLUMN);
+ $stmt->bindParam('table', $table, StudipPDO::PARAM_COLUMN);
+ $stmt->bindParam('userid', $user->id);
+ $stmt->execute();
+ $dbdata = $stmt->fetch(PDO::FETCH_ASSOC);
+ $replacement = str_replace($entry, $dbdata[$column], $replacement);
+ }
+ }
+ // If we have empty values from database, there could be excess whitespace -> remove.
+ return trim(preg_replace('/(\s)+/', ' ', $replacement));
+
+ // The marker is replaced by the result of a function call.
+ case 'function':
+ $data = words($replacement);
+ $function = array_shift($data);
+ return call_user_func_array($function, $data);
+ }
+ }
+
+ /**
+ * Gets a token and assigns it to the given user.
+ */
+ public function getReplacementToken($message_id, $user): string
+ {
+ $token = MassMailToken::findOneBySQL(
+ "`message_id` = :id AND `user_id`IS NULL"
+ );
+
+ if ($token) {
+ $token->user_id = $user->id;
+ $token->store();
+ return $token->token;
+ } else {
+ throw new \Exception('No free token available.');
+ }
+ }
+
+}
diff --git a/lib/models/MassMail/MassMailMessage.php b/lib/models/MassMail/MassMailMessage.php
new file mode 100644
index 0000000..d2cd844
--- /dev/null
+++ b/lib/models/MassMail/MassMailMessage.php
@@ -0,0 +1,373 @@
+<?php
+
+namespace MassMail;
+
+use \Semester, \DBManager, \UserFilter, \Folder, \User, \Config;
+
+class MassMailMessage extends \SimpleORMap implements \UserFilterRange
+{
+
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'massmail_messages';
+
+ $config['serialized_fields']['config'] = \JSONArrayObject::class;
+
+ $config['has_one']['author'] = [
+ 'class_name' => User::class,
+ 'foreign_key' => 'author_id',
+ 'assoc_foreign_key' => 'user_id'
+ ];
+ $config['has_one']['sender'] = [
+ 'class_name' => User::class,
+ 'foreign_key' => 'sender_id',
+ 'assoc_foreign_key' => 'user_id'
+ ];
+ $config['has_many']['filters'] = [
+ 'class_name' => MassMailFilter::class,
+ 'assoc_foreign_key' => 'message_id',
+ 'on_store' => 'store',
+ 'on_delete' => 'delete'
+ ];
+ $config['has_one']['folder'] = [
+ 'class_name' => Folder::class,
+ 'foreign_key' => 'folder_id',
+ 'assoc_foreign_key' => 'id',
+ 'on_store' => 'store',
+ 'on_delete' => 'delete'
+ ];
+ $config['has_many']['tokens'] = [
+ 'class_name' => MassMailToken::class,
+ 'assoc_foreign_key' => 'message_id',
+ 'on_store' => 'store',
+ 'on_delete' => 'delete'
+ ];
+
+ parent::configure($config);
+ }
+
+ /**
+ * Finds all messages that are currently due to be sent.
+ * @return MassMailMessage[]
+ */
+ public static function findUnsent(): array
+ {
+ return static::findBySQL(
+ "`is_template` = 0
+ AND `sent` = 0
+ AND `locked` = 0
+ AND (`send_at_date` IS NULL OR `send_at_date` <= UNIX_TIMESTAMP())
+ ORDER BY `mkdate`"
+ );
+ }
+
+ /**
+ * Finds all messages that have been successfully sent and can be deleted now according to their age.
+ * @return MassMailMessage[]
+ */
+ public static function findObsolete(): array
+ {
+ return static::findBySQL(
+ "`sent` = 1 AND `is_template` = 0 AND `protected` = 0 AND `chdate` <= :threshold",
+ ['threshold' => time() - (Config::get()->MASSMAIL_GC_DAYS * 24 * 60 * 60)]
+ );
+ }
+
+ /**
+ * Possible targets for mass mails.
+ * @return array
+ */
+ public static function getTargets(): array
+ {
+ return [
+ 'all' => _('alle'),
+ 'students' => _('Studierende'),
+ 'employees' => _('Beschäftigte'),
+ 'lecturers' => _('Aktive Lehrende'),
+ 'courses' => _('Veranstaltungen'),
+ 'usernames' => _('Liste von Benutzernamen'),
+ ];
+ }
+
+ /**
+ * Fetches all semesters.
+ * @return array
+ */
+ public static function getSemesters(): array
+ {
+ $semesters = [];
+
+ foreach (array_reverse(Semester::getAll()) as $one) {
+ $semesters[$one->id] = $one->name;
+ }
+
+ return $semesters;
+ }
+
+ /**
+ * Get the folder belonging to this message. If none is found, it will be auto-created as a
+ * personal folder of the current user..
+ * @param string $id
+ * @return \FolderType
+ */
+ public function findFolder(string $id): \FolderType
+ {
+ $messageFolder = Folder::findOneBySQL(
+ "`range_id` = :id AND `range_type` = 'massmail'",
+ ['id' => $id]
+ );
+ if (!$messageFolder) {
+ $messageFolder = new \StandardFolder([
+ 'user_id' => User::findCurrent()->id,
+ 'range_id' => $id,
+ 'range_type' => 'massmail',
+ 'parent_id' => 'root',
+ 'name' => _('Nachricht an Zielgruppen')
+ ]);
+ $messageFolder->store();
+ } else {
+ $messageFolder = $messageFolder->getTypedFolder();
+ }
+
+ return $messageFolder;
+ }
+
+ /**
+ * Gets the real recipient list for this message.
+ * @return string[] the usernames that will get this message.
+ */
+ public function getRecipients(): array
+ {
+ $ids = [];
+
+ switch ($this->target) {
+ // Everyone studying something or working at an institute.
+ case 'all':
+
+ $sql = "SELECT DISTINCT `user_id` FROM `user_studiengang`";
+ $parameters = [];
+ if (!MassMailPermission::has($this->author_id, true)) {
+
+ $permission = MassMailPermission::getForUser($this->author);
+
+ $sql .= " WHERE `abschluss_id` IN (:degrees) OR `fach_id` IN (:subjects)";
+ $parameters = [
+ 'degrees' => $permission['allowed_degrees'],
+ 'subjects' => $permission['allowed_subjects']
+ ];
+ }
+ $students = DBManager::get()->fetchFirst($sql, $parameters);
+
+ $sql = "SELECT DISTINCT `user_id` FROM `user_inst` WHERE `inst_perms` IN (:perms)";
+ $parameters = ['perms' => ['autor', 'tutor', 'dozent']];
+ if (!MassMailPermission::has($this->author_id, true)) {
+ $sql .= " AND `Institut_id` IN (:institutes)";
+ $parameters = [
+ 'institutes' => $permission['allowed_institutes']
+ ];
+ }
+ $employees = DBManager::get()->fetchFirst($sql, $parameters);
+
+ $ids = array_unique(array_merge($students, $employees));
+
+ break;
+
+ // Students are users with at least one studycourse assignment in user_studiengang.
+ case 'students':
+
+ $sql = "SELECT DISTINCT `user_id` FROM `user_studiengang`";
+ $parameters = [];
+
+ if (!MassMailPermission::has($this->author_id, true)) {
+ $permission = MassMailPermission::getForUser($this->author);
+
+ $sql .= " WHERE `abschluss_id` IN (:degrees) OR `fach_id` IN (:subjects)";
+ $parameters = [
+ 'degrees' => $permission['allowed_degrees'],
+ 'subjects' => $permission['allowed_subjects']
+ ];
+ }
+ $ids = DBManager::get()->fetchFirst($sql, $parameters);
+
+ if (count($this->filters) > 0) {
+
+ $filtered = [];
+ foreach ($this->filters as $filter) {
+ $f = new UserFilter($filter->filter_id);
+ $filtered = array_merge($filtered, $f->getUsers());
+ }
+
+ $ids = array_unique(array_intersect($ids, $filtered));
+
+ }
+
+ break;
+
+ // Employees are users with at least one institute assignment at 'autor" level or more.
+ case 'employees':
+
+ $sql = "SELECT DISTINCT `user_id` FROM `user_inst` WHERE `inst_perms` IN (:perms)";
+ $parameters = ['perms' => ['autor', 'tutor', 'dozent']];
+ if (!MassMailPermission::has($this->author_id, true)) {
+ $permission = MassMailPermission::getForUser($this->author);
+
+ $sql .= " AND `Institut_id` IN (:institutes)";
+ $parameters = [
+ 'institutes' => $permission->allowed_institutes ? $permission->allowed_institutes->pluck('id') : []
+ ];
+ }
+ $ids = DBManager::get()->fetchFirst($sql, $parameters);
+
+ if (count($this->filters) > 0) {
+
+ $filtered = [];
+ foreach ($this->filters as $filter) {
+ $f = new UserFilter($filter->filter_id);
+ $filtered = array_merge($filtered, $f->getUsers());
+ }
+
+ $ids = array_unique(array_intersect($ids, $filtered));
+
+ }
+
+ break;
+
+ // Course members having the specified permission level.
+ case 'courses':
+
+ $courses = array_map(
+ fn ($course) => $course['id'],
+ $this->config['courses']->getArrayCopy()
+ );
+ $permission = $this->config['perm'];
+
+ $ids = DBManager::get()->fetchFirst(
+ "SELECT DISTINCT `user_id` FROM `seminar_user` WHERE `Seminar_id` IN (:courses) AND `status` = :perm",
+ ['courses' => $courses, 'perm' => $permission]
+ );
+
+ break;
+
+ // Lecturers of at least one course in the given semester
+ case 'lecturers':
+
+ $ids = DBManager::get()->fetchFirst(
+ "SELECT DISTINCT u.`user_id` FROM `seminar_user` u
+ LEFT JOIN `semester_courses` sc ON (sc.`course_id` = u.`Seminar_id`)
+ JOIN `seminare` s ON (s.`Seminar_id` = u.`Seminar_id`)
+ JOIN `sem_types` t ON (t.`id` = s.`status`)
+ WHERE (sc.`semester_id` = :semester OR sc.`semester_id` IS NULL)
+ AND t.`class` IN (:categories)
+ AND u.`status` = 'dozent'",
+ [
+ 'semester' => $this->config['semester'],
+ 'categories' => Config::get()->MASSMAIL_LECTURER_SEM_CATEGORIES
+ ]
+ );
+
+ break;
+
+ case 'usernames':
+
+ $ids = DBManager::get()->fetchFirst(
+ "SELECT DISTINCT `user_id` FROM `auth_user_md5` WHERE `Username` IN (:usernames)",
+ ['usernames' => explode("\n", $this->config['usernames'])]
+ );
+ }
+
+ return DBManager::get()->fetchFirst(
+ "SELECT DISTINCT `username`
+ FROM `auth_user_md5`
+ WHERE `visible` != :visible
+ AND `locked` = :locked
+ AND `user_id` IN (:ids)
+ AND `username` NOT IN (:exclude)
+ ORDER BY `username`",
+ [
+ 'visible' => 'never',
+ 'locked' => 0,
+ 'ids' => $ids,
+ 'exclude' => $this->exclude_users ? explode("\n", $this->exclude_users) : ['']
+ ]
+ );
+ }
+
+ /**
+ * Checks whether this message has replacement markers in its message text.
+ * @param $with_tokens Check for tokens or just for "normal" markers?
+ * @return bool
+ */
+ public function hasMarkers($type = 'all'): bool
+ {
+ $markers = MassMailMarker::findAndMapBySQL(
+ fn($m) => '{{' . $m->marker . '}}',
+ $type === 'all' ? "1" : "`type` = :type",
+ $type === 'all' ? [] : ['type' => $type]
+ );
+ foreach ($markers as $marker) {
+ if (str_contains($this->message, $marker)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Replaces serial message markers with the data of the given user.
+ * @param User $user
+ * @return string
+ */
+ public function replaceMarkers(User $user): string
+ {
+ $text = MassMailMarker::processText($this->message, $user, $this->getMarkers());
+
+ if (count($this->tokens) > 0) {
+ $text = MassMailMarker::processToken($this->message, $text, $user);
+ }
+
+ return $text;
+ }
+
+ /**
+ * Get available serial message markers, optionally including person token markers
+ * @param bool $with_tokens
+ * @return array
+ */
+ private function getMarkers($with_tokens = true): array
+ {
+ $found = [];
+ $markers = MassMailMarker::findBySQL($with_tokens ? "1" : "`type` != 'token'");
+ foreach ($markers as $marker) {
+ if (str_contains($this->message, $marker->marker)) {
+ $found[] = $marker;
+ }
+ }
+ return $found;
+ }
+
+ /**
+ * Get message attachments (excluding files used fot token generation)
+ * @return array|\FileRef[]
+ */
+ public function getAttachments()
+ {
+ $files = [];
+ $folder = Folder::find($this->folder_id);
+
+ return array_filter(
+ $folder->getTypedFolder()->getFiles(),
+ fn ($ref) => !isset($ref->file->metadata['is_token_file'])
+ );
+ }
+
+ /**
+ * @see UserFilterRange::canEdit()
+ */
+ public function canEditFilter(User $user, UserFilter $filter): bool
+ {
+ return MassMailPermission::has($user->id, true)
+ || MassMailPermission::has($user->id, false) && $this->creator_id === $user->id;
+
+ }
+
+}
diff --git a/lib/models/MassMail/MassMailPermission.php b/lib/models/MassMail/MassMailPermission.php
new file mode 100644
index 0000000..33af43d
--- /dev/null
+++ b/lib/models/MassMail/MassMailPermission.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace MassMail;
+
+class MassMailPermission extends \SimpleORMap
+{
+
+ public const MASSMAIL_ROOT_ROLE = 'Massenmail-Root';
+
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'massmail_permissions';
+
+ $config['belongs_to']['institute'] = [
+ 'class_name' => \Institute::class,
+ 'foreign_key' => 'institute_id',
+ 'assoc_foreign_key' => 'institut_id'
+ ];
+
+ $config['has_and_belongs_to_many']['allowed_degrees'] = [
+ 'class_name' => \Degree::class,
+ 'thru_table' => 'massmail_permission_degree',
+ 'thru_key' => 'permission_id',
+ 'thru_assoc_key' => 'degree_id',
+ 'on_store' => 'store',
+ 'on_delete' => 'delete'
+ ];
+
+ $config['has_and_belongs_to_many']['allowed_subjects'] = [
+ 'class_name' => \StudyCourse::class,
+ 'thru_table' => 'massmail_permission_subject',
+ 'thru_key' => 'permission_id',
+ 'thru_assoc_key' => 'subject_id',
+ 'on_store' => 'store',
+ 'on_delete' => 'delete'
+ ];
+
+ $config['has_and_belongs_to_many']['allowed_institutes'] = [
+ 'class_name' => \Institute::class,
+ 'thru_table' => 'massmail_permission_institute',
+ 'thru_key' => 'permission_id',
+ 'thru_assoc_key' => 'institute_id',
+ 'on_store' => 'store',
+ 'on_delete' => 'delete'
+ ];
+
+ $config['additional_fields']['institute_name']['get'] = function($p) {
+ return $p->institute->name;
+ };
+
+ parent::configure($config);
+ }
+
+ /**
+ * Check if the given user has permissions to write mass mails. The result is cached for performance reasons.
+ *
+ * @param string $user_id user to check
+ * @param bool $unrestricted check for unrestricted permissions
+ * @return bool
+ */
+ public static function has(string $user_id, bool $unrestricted = false) : bool
+ {
+ $cached = \Studip\Cache\Factory::getCache()->read('massmail-permission-' . $user_id);
+
+ if ($cached !== false) {
+ $perm = (int) $cached;
+ } else {
+
+ $perm = 0;
+
+ // Root and users with the massmeil root role are always allowed to do anything.
+ if (
+ $GLOBALS['perm']->have_perm('root', $user_id)
+ || \RolePersistence::isAssignedRole($user_id, static::MASSMAIL_ROOT_ROLE)
+ ) {
+ $perm = 2;
+
+ // Everyone else needs at least one institute assignment with existing permissions.
+ } else {
+ // Institute memberships with existing mass mail permission settings.
+ $relevant = static::findBySQL(
+ "JOIN `user_inst` ON (`user_inst`.`institut_id` = `massmail_permissions`.`institute_id`)
+ WHERE `user_inst`.`inst_perms` != 'user' AND `user_inst`.`user_id` = :user",
+ ['user' => $user_id]
+ );
+ foreach ($relevant as $one) {
+ if ($GLOBALS['perm']->have_studip_perm($one->min_perm, $one->institute_id, $user_id)) {
+ $perm = 1;
+ break;
+ }
+ }
+ }
+
+ \Studip\Cache\Factory::getCache()->write('massmail-permission-' . $user_id, $perm);
+ }
+
+ return $unrestricted ? $perm === 2 : $perm >= 1;
+ }
+
+ /**
+ * @return array{
+ * allowed_degrees: array,
+ * allowed_subjects: array,
+ * allowed_institutes: array
+ * }
+ */
+ public static function getForUser(\User $user, bool $withNames = false): array
+ {
+ // Get user's institutes with at least autor permission.
+ $institutes = $user->institute_memberships->filter(function ($membership) {
+ return in_array($membership->inst_perms, ['autor', 'tutor', 'dozent', 'admin']);
+ })->pluck($withNames ? 'institut_id institute_name' : 'institut_id');
+
+ // Get permission configuration for these institutes.
+ $permissions = static::findBySQL("`institute_id` IN (:institutes)", ['institutes' => $institutes]);
+ $config = [
+ 'allowed_degrees' => [],
+ 'allowed_subjects' => [],
+ 'allowed_institutes' => $institutes
+ ];
+ foreach ($permissions as $permission) {
+ $config['allowed_degrees'] = array_merge(
+ $config['allowed_degrees'],
+ $permission->allowed_degrees->pluck($withNames ? 'id name' : 'id')
+ );
+ $config['allowed_subjects'] = array_merge(
+ $config['allowed_subjects'],
+ $permission->allowed_subjects->pluck($withNames ? 'id name' : 'id')
+ );
+ $config['allowed_institutes'] = array_merge(
+ $config['allowed_institutes'],
+ $permission->allowed_institutes->pluck($withNames ? 'id name' : 'id')
+ );
+ }
+
+ return $config;
+ }
+
+}
diff --git a/lib/models/MassMail/MassMailToken.php b/lib/models/MassMail/MassMailToken.php
new file mode 100644
index 0000000..aacfe4b
--- /dev/null
+++ b/lib/models/MassMail/MassMailToken.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace MassMail;
+
+class MassMailToken extends \SimpleORMap
+{
+
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'massmail_tokens';
+
+ $config['belongs_to']['message'] = [
+ 'class_name' => MassMailMessage::class,
+ 'foreign_key' => 'message_id'
+ ];
+
+ $config['belongs_to']['user'] = [
+ 'class_name' => \User::class,
+ 'foreign_key' => 'user_id'
+ ];
+
+ parent::configure($config);
+ }
+
+}
diff --git a/lib/navigation/MessagingNavigation.php b/lib/navigation/MessagingNavigation.php
index 563b760..77405a0 100644
--- a/lib/navigation/MessagingNavigation.php
+++ b/lib/navigation/MessagingNavigation.php
@@ -31,7 +31,7 @@ class MessagingNavigation extends Navigation
parent::initItem();
$my_messaging_settings = UserConfig::get($user->id)->MESSAGING_SETTINGS;
$lastVisitedTimestamp = isset($my_messaging_settings['last_box_visit'])?(int)$my_messaging_settings['last_box_visit']:0;
-
+
$query = "SELECT SUM(mkdate > :time AND readed = 0) AS num_new,
SUM(readed = 0) AS num_unread,
SUM(readed = 1) AS num_read
@@ -42,7 +42,7 @@ class MessagingNavigation extends Navigation
$statement->bindValue(':user_id', $GLOBALS['user']->id);
$statement->execute();
list($neux, $neum, $altm) = $statement->fetch(PDO::FETCH_NUM);
-
+
$this->setBadgeNumber($neum);
if ($neux > 0) {
@@ -69,12 +69,35 @@ class MessagingNavigation extends Navigation
public function initSubNavigation()
{
parent::initSubNavigation();
-
+
$messages = new Navigation(_('Nachrichten'), 'dispatch.php/messages/overview');
$inbox = new Navigation(_('Eingang'), 'dispatch.php/messages/overview');
$messages->addSubNavigation('inbox', $inbox);
$messages->addSubNavigation('sent', new Navigation(_('Gesendet'), 'dispatch.php/messages/sent'));
$this->addSubNavigation('messages', $messages);
-
+
+ if ($GLOBALS['perm']->have_perm('tutor') && \MassMail\MassMailPermission::has(User::findCurrent()->id)) {
+ $massmail = new Navigation(_('Nachrichten an Zielgruppen'), 'dispatch.php/massmail/message');
+ $massmail->addSubNavigation(
+ 'message',
+ new Navigation(_('Nachricht schreiben'), 'dispatch.php/massmail/message')
+ );
+ $massmail->addSubNavigation(
+ 'overview',
+ new Navigation(_('Nachrichtenübersicht'), 'dispatch.php/massmail/overview')
+ );
+ if (\MassMail\MassMailPermission::has(User::findCurrent()->id, true)) {
+ $massmail->addSubNavigation(
+ 'permissions',
+ new Navigation(_('Berechtigungen'), 'dispatch.php/massmail/permissions')
+ );
+ $massmail->addSubNavigation(
+ 'settings',
+ new Navigation(_('Einstellungen'), 'dispatch.php/massmail/settings')
+ );
+ }
+ $this->addSubNavigation('massmail', $massmail);
+ }
+
}
}
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index ec18d59..567e9ff 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -6,12 +6,15 @@ const BaseComponents = {
Datetimepicker: () => import('./components/Datetimepicker.vue'),
DayOfWeekSelect: () => import('./components/form_inputs/DayOfWeekSelect.vue'),
EditableList: () => import("./components/EditableList.vue"),
+ FileUpload: () => import('./components/form_inputs/FileUpload.vue'),
I18nTextarea: () => import("./components/I18nTextarea.vue"),
Multiselect: () => import('./components/Multiselect.vue'),
MyCoursesColouredTable: () => import('./components/form_inputs/MyCoursesColouredTable.vue'),
Quicksearch: () => import('./components/Quicksearch.vue'),
+ QuicksearchListInput: () => import('./components/form_inputs/QuicksearchListInput.vue'),
RangeInput: () => import('./components/RangeInput.vue'),
RepetitionInput: () => import("./components/form_inputs/RepetitionInput.vue"),
+ SerialTextMarkers: () => import('./components/form_inputs/SerialTextMarkers.vue'),
SidebarWidget: () => import('./components/SidebarWidget.vue'),
StudipActionMenu: () => import('./components/StudipActionMenu.vue'),
StudipAssetImg: () => import('./components/StudipAssetImg.vue'),
@@ -27,6 +30,7 @@ const BaseComponents = {
StudipSelect: () => import('./components/StudipSelect.vue'),
StudipTooltipIcon: () => import('./components/StudipTooltipIcon.vue'),
StudipWysiwyg: () => import("./components/StudipWysiwyg.vue"),
+ UserFilterInput: () => import('./components/form_inputs/UserFilterInput.vue')
};
export default BaseComponents;
diff --git a/resources/vue/components/StudipUserFilter.vue b/resources/vue/components/StudipUserFilter.vue
index f9e6741..20ca162 100644
--- a/resources/vue/components/StudipUserFilter.vue
+++ b/resources/vue/components/StudipUserFilter.vue
@@ -65,6 +65,14 @@ export default {
filter: {
type: Array,
default: () => []
+ },
+ context: {
+ type: String,
+ default: ''
+ },
+ target: {
+ type: String,
+ default: ''
}
},
data() {
@@ -119,7 +127,17 @@ export default {
}
},
created() {
- STUDIP.jsonapi.withPromises().get('user-filter-fields').then(response => {
+ STUDIP.jsonapi.withPromises().get(
+ 'user-filter-fields',
+ {
+ data: {
+ filter: {
+ context: this.context,
+ target: this.target
+ }
+ }
+ }
+ ).then(response => {
this.availableFields = response.data;
this.addField();
});
diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/StudipWysiwyg.vue
index 799c5f1..3b36cc8 100644
--- a/resources/vue/components/StudipWysiwyg.vue
+++ b/resources/vue/components/StudipWysiwyg.vue
@@ -59,6 +59,8 @@ export default {
if (this.shouldFocus) {
this.focus();
}
+
+ STUDIP.eventBus.emit('editor-loaded', this.createdEditor);
},
onInput(value) {
this.currentText = value;
diff --git a/resources/vue/components/form_inputs/FileUpload.vue b/resources/vue/components/form_inputs/FileUpload.vue
new file mode 100644
index 0000000..b512c02
--- /dev/null
+++ b/resources/vue/components/form_inputs/FileUpload.vue
@@ -0,0 +1,198 @@
+<template>
+ <section>
+ <button v-show="!uploading"
+ class="button select"
+ :class="{studiprequired: required}"
+ @click.prevent="openFileSelect">
+ <studip-icon shape="upload"></studip-icon>
+ <span class="textlabel">
+ {{ title }}
+ </span>
+ <span v-if="required"
+ class="asterisk"
+ :title="$gettext('Dies ist ein Pflichtfeld')"
+ aria-hidden="true">*</span>
+ </button>
+ <div class="file-count">
+ <template v-if="selectedFiles?.length === 0">
+ {{ $gettext('Keine Dateien gewählt') }}
+ </template>
+ <template v-else-if="selectedFiles?.length === 1">
+ {{ $gettext('Eine Datei gewählt') }}
+ </template>
+ <template v-else>
+ {{ $gettextInterpolate($gettext('%{number} Dateien gewählt'), { number: selectedFiles.length }) }}
+ </template>
+ </div>
+ <input type="file"
+ :name="name"
+ :id="id"
+ :multiple="multiple"
+ :accept="accept"
+ ref="files"
+ class="button"
+ @change="selectFiles">
+ <button v-if="selectedFiles.length > 0"
+ type="button"
+ class="button upload"
+ @click.prevent="upload">
+ <studip-icon shape="upload"></studip-icon>
+ {{ $gettext('Jetzt hochladen') }}
+ </button>
+ <div v-if="!uploading && uploadedFiles.length > 0">
+ <span>
+ {{ $gettext('Bereits hochgeladen:') }}
+ </span>
+ <ul>
+ <li v-for="(file, index) in uploadedFiles"
+ :key="index">
+ {{ file.name + ' (' + getTextualFileSize(file.size) + ')' }}
+ </li>
+ </ul>
+ </div>
+ <input type="hidden"
+ :name="name"
+ :value="targetFolder">
+ <studip-progress-indicator v-if="uploading"
+ :size="24">
+ {{ $gettext('Wird hochgeladen...') }}
+ </studip-progress-indicator>
+ </section>
+</template>
+
+<script>
+import axios from 'axios';
+import StudipProgressIndicator from "../StudipProgressIndicator.vue";
+
+export default {
+ name: 'FileUpload',
+ components: {StudipProgressIndicator},
+ props: {
+ name: {
+ type: String,
+ required: true
+ },
+ title: {
+ type: String,
+ required: true
+ },
+ folder: {
+ type: String,
+ required: true
+ },
+ uploadUrl: {
+ type: String,
+ required: true
+ },
+ id: {
+ type: String
+ },
+ required: {
+ type: Boolean,
+ default: false
+ },
+ multiple: {
+ type: Boolean,
+ default: false
+ },
+ accept: {
+ type: String,
+ default: '*/*'
+ }
+ },
+ data() {
+ return {
+ selectedFiles: [],
+ uploading: false,
+ uploadedFiles: [],
+ targetFolder: ''
+ }
+ },
+ methods: {
+ upload() {
+ if (this.$refs.files.files.length > 0) {
+ this.uploading = true;
+
+ const files = this.$refs.files.files;
+
+ const formData = new FormData();
+
+ let name = this.name;
+ if (this.multiple) {
+ name += '[]';
+ }
+
+ for (let i = 0; i < files.length; i++) {
+ formData.append(name, files[i]);
+ this.uploadedFiles.push(files[i]);
+ }
+
+ axios.post(
+ this.uploadUrl,
+ formData
+ ).then(response => {
+ this.uploading = false;
+ this.$refs.files.value = '';
+ this.targetFolder = this.folder;
+ }).catch(error => {
+ this.uploadedFiles = [];
+ this.uploading = false;
+ STUDIP.Report.error(this.$gettext('Fehler beim Hochladen'), error);
+ });
+ }
+ },
+ getTextualFileSize(bytes) {
+ let unit = '';
+ let context = {size: bytes};
+ if (bytes < 1024) {
+ unit = this.$gettext('%{size} B');
+ } else if (bytes < 1024 * 1024) {
+ unit = this.$gettext('%{size} KB');
+ context.size = (bytes / 1024).toFixed(2);
+ } else {
+ unit = this.$gettext('%{size} MB');
+ context.size = (bytes / (1024 * 1024)).toFixed(2);
+ }
+
+ return this.$gettextInterpolate(unit, context);
+ },
+ openFileSelect() {
+ this.$refs.files.click();
+ },
+ selectFiles() {
+ this.selectedFiles = this.$refs.files.files;
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+input[type=file] {
+ display: none;
+}
+button {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding: 5px 10px;
+
+ img {
+ margin-right: 5px;
+ vertical-align: text-bottom;
+ }
+}
+.select {
+ margin-right: 0;
+}
+.file-count {
+ border: solid thin var(--light-gray-color-40);
+ border-left: unset;
+ display: inline-block;
+ margin-left: -4px;
+ padding: 5px 10px 4px 10px;
+ position: relative;
+ top: 2px;
+}
+.upload {
+ margin-left: 15px;
+}
+</style>
diff --git a/resources/vue/components/form_inputs/QuicksearchListInput.vue b/resources/vue/components/form_inputs/QuicksearchListInput.vue
new file mode 100644
index 0000000..4a3e21c
--- /dev/null
+++ b/resources/vue/components/form_inputs/QuicksearchListInput.vue
@@ -0,0 +1,97 @@
+<template>
+ <div>
+ <quicksearch :searchtype="searchtype"
+ :autocomplete="autocomplete"
+ @input="addElement"></quicksearch>
+ <table v-if="elements.length > 0" ref="results" class="default">
+ <tbody>
+ <tr v-for="(element, index) in elements"
+ :key="element.id">
+ <td>
+ {{ element.name }}
+ </td>
+ <td class="actions">
+ <a @click="removeElement(index)"
+ :title="$gettext('Dieses Element entfernen')">
+ <studip-icon shape="trash"></studip-icon>
+ </a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <input type="hidden"
+ :name="name"
+ :value="realValue"
+ >
+ </div>
+</template>
+
+<script>
+import quicksearch from '../Quicksearch.vue';
+
+export default {
+ name: 'QuicksearchList',
+ components: [ quicksearch ],
+ props: {
+ name: {
+ type: String,
+ required: true
+ },
+ value: {
+ type: String,
+ default: ''
+ },
+ searchtype: {
+ type: String,
+ required: true
+ },
+ autocomplete: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return {
+ elements: []
+ }
+ },
+ computed: {
+ realValue() {
+ this.$emit('input', JSON.stringify(this.elements));
+ return JSON.stringify(this.elements);
+ }
+ },
+ methods: {
+ addElement(id, name) {
+ if (!this.elements.map(e => e.id).includes(id)) {
+ const element = {
+ id: id,
+ name: name
+ };
+ this.elements.push(element);
+ }
+ },
+ removeElement(index) {
+ this.elements.splice(index, 1);
+ }
+ },
+ created() {
+ if (this.value !== '') {
+ this.elements = JSON.parse(this.value);
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+table.default {
+ margin-bottom: unset;
+ margin-top: 15px;
+ width: 50%;
+
+ .actions {
+ text-align: right;
+ }
+}
+
+</style>
diff --git a/resources/vue/components/form_inputs/SerialTextMarkers.vue b/resources/vue/components/form_inputs/SerialTextMarkers.vue
new file mode 100644
index 0000000..20e542d
--- /dev/null
+++ b/resources/vue/components/form_inputs/SerialTextMarkers.vue
@@ -0,0 +1,80 @@
+<template>
+ <div>
+ <label class="col-3">
+ {{ $gettext('Feld für Serienmail einfügen') }}
+ <select v-model="selectedMarker">
+ <option value="">
+ -- {{ $gettext('Feld zum Einfügen auswählen') }} --
+ </option>
+ <option v-for="(marker, index) in markers"
+ :key="index"
+ :value="marker.marker"
+ :data-description="marker.description">
+ {{ marker.name }}
+ </option>
+ </select>
+ </label>
+ <button class="button col-3 insert-marker-button"
+ :title="$gettext('Feld einfügen')"
+ :disabled="selectedMarker === ''"
+ @click.prevent="insertMarker">
+ {{ $gettext('In den Text einfügen') }}
+ </button>
+ <p v-if="selectedMarker !== ''">
+ {{ description }}
+ </p>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'SerialTextMarkers',
+ props: {
+ markers: {
+ type: Array,
+ required: true
+ },
+ editor: {
+ type: String,
+ required: true
+ }
+ },
+ data() {
+ return {
+ editorInstance: null,
+ selectedMarker: ''
+ }
+ },
+ computed: {
+ description() {
+ return this.markers.find((m) => { return m.marker === this.selectedMarker; }).description;
+ }
+ },
+ methods: {
+ insertMarker() {
+ this.editorInstance.model.change(writer => {
+ writer.insertText(
+ ' {{' + this.selectedMarker + '}}',
+ this.editorInstance.model.document.selection.getFirstPosition()
+ );
+ });
+ }
+ },
+ mounted() {
+ STUDIP.eventBus.on('editor-loaded', editor => {
+ if (document.getElementById(this.editor) === editor.sourceElement) {
+ this.editorInstance = editor;
+ }
+ });
+ },
+ destroyed() {
+ STUDIP.eventBus.off('editor-loaded');
+ }
+}
+</script>
+
+<style scoped>
+button {
+ vertical-align: bottom;
+}
+</style>
diff --git a/resources/vue/components/form_inputs/UserFilterInput.vue b/resources/vue/components/form_inputs/UserFilterInput.vue
new file mode 100644
index 0000000..d63e97b
--- /dev/null
+++ b/resources/vue/components/form_inputs/UserFilterInput.vue
@@ -0,0 +1,146 @@
+<template>
+ <div class="formpart">
+ <section v-if="filters.length > 0" class="default userfilter-list">
+ <header>
+ <h2>
+ {{ $gettext('Mindestens ein Filter muss zutreffen') }}
+ </h2>
+ </header>
+ <table class="default">
+ <tbody>
+ <tr v-for="(filter, index) in filters"
+ :key="index"
+ class="userfilter">
+ <td v-html="filter.attributes.text"></td>
+ <td class="actions">
+ <a class="undecorated"
+ @click.prevent="deleteFilter(index)"
+ :title="$gettext('Diesen Filter löschen')"
+ tabindex="0">
+ <studip-icon shape="trash"></studip-icon>
+ </a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <button class="button"
+ type="button"
+ @click.prevent="editFilter(0)">
+ {{ $gettext('Filter hinzufügen') }}
+ </button>
+ <studip-user-filter v-if="currentFilter !== null"
+ :filter="currentFilter !== 0 ? filters[currentFilter] : []"
+ :context="context"
+ :target="target"
+ @submit="submitFilter"
+ @close="closeFilter"></studip-user-filter>
+ </div>
+</template>
+
+<script>
+import StudipUserFilter from '../StudipUserFilter.vue';
+
+export default {
+ name: 'UserFilterInput',
+ components: {StudipUserFilter},
+ props: {
+ name: {
+ type: String,
+ required: true
+ },
+ value: String,
+ context: {
+ type: String,
+ default: ''
+ },
+ target: {
+ type: String,
+ default: 'all'
+ }
+ },
+ data() {
+ return {
+ key: 0,
+ currentFilter: null,
+ filters: [],
+ stringified: ''
+ }
+ },
+ methods: {
+ editFilter(index) {
+ this.currentFilter = index;
+ },
+ submitFilter(filter) {
+ STUDIP.jsonapi.withPromises().post(
+ 'user-filters',
+ {
+ data: {
+ data: {
+ attributes: {
+ filters: filter
+ }
+ }
+ }
+ })
+ .then(response => {
+ if (this.currentFilter !== 0) {
+ this.filters[this.currentFilter] = response.data;
+ } else {
+ this.filters.push(response.data);
+ }
+ this.currentFilter = null;
+ this.changed();
+ })
+ .catch(error => {
+ STUDIP.Report.error(this.$gettext('Es ist ein Fehler aufgetreten'), error);
+ });
+ },
+ closeFilter() {
+ this.currentFilter = null;
+ },
+ deleteFilter(index) {
+ this.filters.splice(index, 1);
+ this.changed();
+ },
+ actionMenuItems(index) {
+ return [
+ {
+ id: 'edit',
+ label: this.$gettext('Bearbeiten'),
+ icon: 'edit',
+ emit: 'edit',
+ emitArguments: index
+ },
+ {
+ id: 'delete',
+ label: this.$gettext('Löschen'),
+ icon: 'trash',
+ emit: 'delete',
+ emitArguments: index
+ }
+ ];
+ },
+ changed() {
+ this.stringified = JSON.stringify(this.filters);
+ this.$emit('input', this.stringified);
+ }
+ },
+ mounted() {
+ if (this.value) {
+ this.filters = JSON.parse(this.value);
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+table.default {
+ margin-bottom: unset;
+ width: 50%;
+
+ .actions {
+ text-align: right;
+ }
+}
+</style>
diff --git a/resources/vue/components/massmail/MassMailMessagesList.vue b/resources/vue/components/massmail/MassMailMessagesList.vue
new file mode 100644
index 0000000..9d71445
--- /dev/null
+++ b/resources/vue/components/massmail/MassMailMessagesList.vue
@@ -0,0 +1,153 @@
+<template>
+ <div>
+ <studip-progress-indicator v-if="loading"
+ :size="32"
+ />
+ <table v-else-if="messages.data?.length > 0" class="default">
+ <colgroup>
+ <col>
+ <col>
+ <col>
+ <col>
+ <col style="width: 200px">
+ <col style="width: 20px">
+ </colgroup>
+ <thead>
+ <tr>
+ <th>{{ $gettext('Betreff') }}</th>
+ <th>{{ $gettext('Nachricht') }}</th>
+ <th>{{ $gettext('Erstellt von') }}</th>
+ <th>{{ $gettext('Zielgruppe') }}</th>
+ <th>{{ $gettext('Letzte Änderung') }}</th>
+ <th>{{ $gettext('Aktionen') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(message, index) in messages.data"
+ :key="index"
+ >
+ <td>{{ message.attributes.subject }}</td>
+ <td v-html="message.attributes.message"></td>
+ <td>{{ getAuthor(message.relationships.author.data.id).attributes['formatted-name'] }}</td>
+ <td>{{ message.attributes.target }}</td>
+ <td>{{ message.attributes.chdate }}</td>
+ <td>
+ <studip-action-menu :items="actionMenuItems"
+ @edit="editMessage(message.id)"
+ @delete="deleteMessage(message.id)"></studip-action-menu>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <studip-message-box v-else
+ type="info">
+ {{ $gettext('Es wurden keine Nachrichten gefunden.') }}
+ </studip-message-box>
+ <mounting-portal mount-to="#message-views">
+ <sidebar-widget id="views-widget" class="sidebar-widget" :title="$gettext('Ansichten')">
+ <template #content>
+ <ul class="widget-list widget-links sidebar-views"
+ :aria-label="$gettext('Ansichten')">
+ <li id="index" :class="{ active: 'unsent' === currentView}">
+ <a :href="url('dispatch.php/massmail/overview')"
+ @click.prevent="setCurrentView('unsent')">
+ {{ $gettext('Zum Versand anstehend') }}
+ </a>
+ </li>
+ <li id="index" :class="{ active: 'templates' === currentView}">
+ <a :href="url('dispatch.php/massmail/overview')"
+ @click.prevent="setCurrentView('templates')">
+ {{ $gettext('Meine Vorlagen') }}
+ </a>
+ </li>
+ <li id="index" :class="{ active: 'protected' === currentView}">
+ <a :href="url('dispatch.php/massmail/overview')"
+ @click.prevent="setCurrentView('protected')">
+ {{ $gettext('Geschützt') }}
+ </a>
+ </li>
+ </ul>
+ </template>
+ </sidebar-widget>
+ </mounting-portal>
+ </div>
+</template>
+
+<script>
+import StudipProgressIndicator from '../StudipProgressIndicator.vue';
+import StudipActionMenu from '../StudipActionMenu.vue';
+import SidebarWidget from '../SidebarWidget.vue';
+
+export default {
+ name: 'MassMailMessagesList',
+ components: { SidebarWidget, StudipActionMenu, StudipProgressIndicator },
+ data() {
+ return {
+ loading: false,
+ messages: {},
+ currentView: 'unsent'
+ }
+ },
+ computed: {
+ actionMenuItems() {
+ return [
+ { label: this.$gettext('Bearbeiten'), icon: 'edit', emit: 'edit'},
+ { label: this.$gettext('Löschen'), icon: 'trash', emit: 'delete'}
+ ];
+ }
+ },
+ methods: {
+ getMessages() {
+ this.loading = true;
+
+ const data = { include: 'author'};
+
+ switch (this.currentView) {
+ case 'templates':
+ data.filter = {templates: 1};
+ break;
+ case 'protected':
+ data.filter = {protected: 1};
+ break;
+ }
+
+ STUDIP.jsonapi.withPromises().get('mass-mails/messages', {data: data})
+ .then(response => {
+ this.messages = response;
+ this.loading = false;
+ })
+ .catch(error => {
+ this.messages = [];
+ STUDIP.Report.error(this.$gettext('Es ist ein Fehler aufgetreten'), error);
+ this.loading = false;
+ });
+ },
+ getAuthor(id) {
+ const result = this.messages.included.filter(entry => entry.id === id);
+ return result?.length > 0 ? result[0] : null;
+ },
+ editMessage(id) {
+ window.location = STUDIP.URLHelper.getURL('dispatch.php/massmail/message/index/' + id);
+ },
+ deleteMessage(id) {
+ if (STUDIP.Dialog.confirm(
+ this.$gettext('Soll diese Nachricht wirklich gelöscht werden?'),
+ () => {
+ window.location = STUDIP.URLHelper.getURL('dispatch.php/massmail/message/delete/' + id);
+ },
+ STUDIP.Dialog.close())
+ );
+ },
+ url(target) {
+ return STUDIP.URLHelper.getURL(target);
+ },
+ setCurrentView(view) {
+ this.currentView = view;
+ this.getMessages();
+ }
+ },
+ created() {
+ this.getMessages();
+ }
+}
+</script>
diff --git a/resources/vue/components/massmail/MassMailPermissions.vue b/resources/vue/components/massmail/MassMailPermissions.vue
new file mode 100644
index 0000000..6342427
--- /dev/null
+++ b/resources/vue/components/massmail/MassMailPermissions.vue
@@ -0,0 +1,108 @@
+<template>
+ <div>
+ <table v-if="!loading && permissions?.data.length > 0"
+ class="default">
+ <colgroup>
+ <col>
+ <col width="20%">
+ <col width="30%">
+ <col width="24">
+ </colgroup>
+ <thead>
+ <tr>
+ <th>{{ $gettext('Einrichtung') }}</th>
+ <th>{{ $gettext('Benötigte Rechte') }}</th>
+ <th>{{ $gettext('Erlaubte Zielgruppen') }}</th>
+ <th>{{ $gettext('Aktionen') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(permission) in permissions.data" :key="permission.id">
+ <td>
+ {{ getInstitute(permission).attributes.name }}
+ </td>
+ <td>
+ {{ permission.attributes['min-perm']}}
+ </td>
+ <td>
+ <div v-if="permission.meta['allowed-degrees-count'] > 0">
+ {{ $gettextInterpolate($gettext('%{degrees} Abschlüsse'), { degrees: permission.meta['allowed-degrees-count']}) }}
+ </div>
+ <div v-if="permission.meta['allowed-subjects-count'] > 0">
+ {{ $gettextInterpolate($gettext('%{subjects} Fächer'), { subjects: permission.meta['allowed-subjects-count']}) }}
+ </div>
+ <div v-if="permission.meta['allowed-institutes-count'] > 0">
+ {{ $gettextInterpolate($gettext('%{institutes} Einrichtungen'), { institutes: permission.meta['allowed-institutes-count']}) }}
+ </div>
+ </td>
+ <td>
+ <studip-action-menu :items="actionMenuItems"
+ @edit="editPermission(permission.id)"
+ @delete="deletePermission(permission.id)"></studip-action-menu>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <studip-message-box v-if="!loading && permissions.data.length === 0" type="info">
+ {{ $gettext('Es sind keine Berechtigungen für Personen ohne Root-Rechte konfiguriert.') }}
+ </studip-message-box>
+ <studip-progress-indicator v-if="loading"></studip-progress-indicator>
+ </div>
+</template>
+
+<script>
+import StudipProgressIndicator from "../StudipProgressIndicator.vue";
+import StudipActionMenu from "../StudipActionMenu.vue";
+
+export default {
+ name: 'MassMailPermissions',
+ components: {StudipActionMenu, StudipProgressIndicator},
+ data() {
+ return {
+ loading: true,
+ permissions: []
+ }
+ },
+ computed: {
+ actionMenuItems() {
+ return [
+ { label: this.$gettext('Bearbeiten'), icon: 'edit', emit: 'edit'},
+ { label: this.$gettext('Löschen'), icon: 'trash', emit: 'delete'}
+ ];
+ }
+ },
+ methods: {
+ getInstitute(permission) {
+ const institute = this.permissions.included.filter(entry => {
+ return entry.id === permission.relationships.institute.data.id;
+ });
+ return institute.at(0);
+ },
+ editPermission(id) {
+ STUDIP.Dialog.fromURL(
+ STUDIP.URLHelper.getURL('dispatch.php/massmail/permissions/edit/' + id)
+ );
+ },
+ deletePermission(id) {
+ if (STUDIP.Dialog.confirm(
+ this.$gettext('Soll diese Berechtigung wirklich gelöscht werden?'),
+ () => {
+ window.location = STUDIP.URLHelper.getURL('dispatch.php/massmail/permissions/delete/' + id);
+ location.reload();
+ },
+ STUDIP.Dialog.close())
+ );
+ }
+ },
+ created() {
+ STUDIP.jsonapi.GET('mass-mails/permissions', { data: { include: 'institute'}})
+ .then(response => {
+ this.permissions = response;
+ this.loading = false;
+ })
+ .fail(error => {
+ STUDIP.Report.error(error);
+ });
+ }
+}
+</script>
diff --git a/templates/forms/checkbox_collection_input.php b/templates/forms/checkbox_collection_input.php
new file mode 100644
index 0000000..9e6dec3
--- /dev/null
+++ b/templates/forms/checkbox_collection_input.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @var bool $collapsible
+ * @var string $title
+ * @var array $options
+ * @var bool $required
+ * @var string $name
+ * @var array $selected
+ * @var array $attributes
+ */
+?>
+<fieldset<?= $collapsable ? ' class="collapsable collapsed"' : '' ?>>
+ <legend><?= htmlReady($title) ?></legend>
+ <? foreach ($options as $id => $displayname): ?>
+ <label<?= $required ? ' class="studiprequired"' : '' ?>>
+ <input type="checkbox"
+ v-model="<?= htmlReady($name) ?>"
+ name="<?= htmlReady($name) ?>[]"
+ value="<?= $id ?>"
+ class="<?= htmlReady($name . '-selector') ?>"
+ id="<?= $id ?>"
+ <?= $required ? 'required aria-required="true"' : '' ?>
+ <?= in_array($id, $selected) ? 'selected' : '' ?>
+ <?= $attributes ?>>
+ <span class="textlabel">
+ <?= htmlReady($displayname) ?>
+ </span>
+ <? if ($required) : ?>
+ <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+ <? endif ?>
+ </label>
+ <? endforeach ?>
+</fieldset>
diff --git a/templates/forms/fieldset.php b/templates/forms/fieldset.php
index 491f726..25a9739 100644
--- a/templates/forms/fieldset.php
+++ b/templates/forms/fieldset.php
@@ -1,4 +1,12 @@
-<fieldset>
+<?php
+/**
+ * @var bool $collapsable
+ * @var bool $collapsed
+ * @var string $legend
+ * @var array<\Studip\Forms\Part> $part
+ */
+?>
+<fieldset<?= $collapsable ? ' class="collapsable' . ($collapsed ? ' collapsed' : '') . '"' : '' ?>>
<? if ($legend) : ?>
<legend><?= htmlReady($this->legend) ?></legend>
<? endif ?>
diff --git a/templates/forms/file_input.php b/templates/forms/file_input.php
new file mode 100644
index 0000000..2441b52
--- /dev/null
+++ b/templates/forms/file_input.php
@@ -0,0 +1,11 @@
+<div class="formpart" data-form-input-for="<?= htmlReady($name) ?>">
+ <file-upload
+ name="<?= htmlReady($name) ?>"
+ title="<?= htmlReady($title) ?>"
+ upload-url="<?= htmlReady($uploadUrl) ?>"
+ folder="<?= htmlReady($value) ?>"
+ id="<?= htmlReady($id) ?>"
+ :multiple="<?= $multiple ? 'true' : 'false' ?>"
+ accept="<?= htmlReady($accept) ?>"
+ <?= $required ? ':required="true"' : '' ?>></file-upload>
+</div>
diff --git a/templates/forms/form.php b/templates/forms/form.php
index b367974..00e4c7d 100644
--- a/templates/forms/form.php
+++ b/templates/forms/form.php
@@ -37,6 +37,7 @@ $form_id = md5(uniqid());
data-required="<?= htmlReady(json_encode($required_inputs)) ?>"
data-server_validation="<?= $server_validation ? 1 : 0?>"
data-validation_url="<?= htmlReady($_SERVER['REQUEST_URI']) ?>"
+ <?= $form->hasFileInput() ? ' enctype="application/x-www-form-urlencoded"' : '' ?>
class="default studipform<?= $form->isCollapsable() ? ' collapsable' : '' ?>">
<?= CSRFProtection::tokenTag(['ref' => 'securityToken']) ?>
diff --git a/templates/forms/quicksearchlist_input.php b/templates/forms/quicksearchlist_input.php
new file mode 100644
index 0000000..167ec93
--- /dev/null
+++ b/templates/forms/quicksearchlist_input.php
@@ -0,0 +1,18 @@
+<div class="formpart" data-form-input-for="<?= htmlReady($name) ?>">
+ <label<?= $required ? ' class="studiprequired"' : '' ?> for="<?= htmlReady($id) ?>">
+ <span class="textlabel">
+ <?= htmlReady($title) ?>
+ </span>
+ <? if ($required) : ?>
+ <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+ <? endif ?>
+ <quicksearch-list-input
+ name="<?= htmlReady($name) ?>"
+ id="<?= htmlReady($id) ?>"
+ <?= $required ? 'required aria-required="true"' : '' ?>
+ value="<?= htmlReady($value) ?>"
+ v-model="<?= htmlReady($name) ?>"
+ <?= $attributes ?>
+ ></quicksearch-list-input>
+ </label>
+</div>
diff --git a/templates/forms/radio_input.php b/templates/forms/radio_input.php
index da110d1..f7636d1 100644
--- a/templates/forms/radio_input.php
+++ b/templates/forms/radio_input.php
@@ -1,17 +1,21 @@
<div class="formpart">
<section <?= $this->orientation == 'horizontal' ? 'class="hgroup"' : '' ?> id="<?= htmlReady($id) ?>">
- <span class="textlabel">
+ <span class="textlabel<?= $required ? ' studiprequired' : '' ?> ">
<?= htmlReady($this->title) ?>
+ <? if ($required) : ?>
+ <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+ <? endif ?>
</span>
- <? foreach ($options as $key => $option) : ?>
+ <? $count = 0; foreach ($options as $key => $option) : ?>
<label class="" <?= $attributes ?>>
<input type="radio"
- name="<?= htmlReady($this->name) ?>"
- v-model="<?= htmlReady($this->name) ?>"
- value="<?= htmlReady($key) ?>" <?= $key == $value ? 'checked' : '' ?>>
+ name="<?= htmlReady($name) ?>"
+ v-model="<?= htmlReady($name) ?>"
+ value="<?= htmlReady($key) ?>" <?= $key == $value ? 'checked' : '' ?>
+ <?= $required && $count === 0 ? ' required' : ''?>>
<?= htmlReady($option) ?>
</label>
- <? endforeach ?>
+ <? $count++; endforeach ?>
</section>
</div>
diff --git a/templates/forms/serial_wysiwyg_input.php b/templates/forms/serial_wysiwyg_input.php
new file mode 100644
index 0000000..f67a0c1
--- /dev/null
+++ b/templates/forms/serial_wysiwyg_input.php
@@ -0,0 +1,17 @@
+<div class="formpart" data-form-input-for="<?= htmlReady($name) ?>">
+ <label<?= $this->required ? ' class="studiprequired"' : '' ?> for="<?= htmlReady($id) ?>">
+ <span class="textlabel">
+ <?= htmlReady($this->title) ?>
+ </span>
+ <? if ($this->required) : ?>
+ <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+ <? endif ?>
+ </label>
+ <serial-text-markers :markers="<?= htmlReady($markers) ?>" editor="<?= htmlReady($id) ?>"></serial-text-markers>
+ <studip-wysiwyg
+ id="<?= htmlReady($id) ?>"
+ v-model="<?= htmlReady($name) ?>"
+ value="<?= htmlReady($value) ?>"
+ <?= $required ? 'required' : '' ?>>
+ </studip-wysiwyg>
+</div>
diff --git a/templates/forms/textarea_input.php b/templates/forms/textarea_input.php
index 2afa96c..8effff1 100644
--- a/templates/forms/textarea_input.php
+++ b/templates/forms/textarea_input.php
@@ -1,13 +1,15 @@
-<label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>">
- <span class="textlabel">
- <?= htmlReady($this->title) ?>
- </span>
- <? if ($this->required) : ?>
- <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
- <? endif ?>
-</label>
-<textarea name="<?= htmlReady($name) ?>"
- v-model="<?= htmlReady($name) ?>"
- id="<?= $id ?>"
- <?= ($required ? 'required aria-required="true"' : '') ?>
- <?= $attributes ?>><?= htmlReady($value) ?></textarea>
+<div class="formpart" data-form-input-for="<?= htmlReady($name) ?>">
+ <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>">
+ <span class="textlabel">
+ <?= htmlReady($this->title) ?>
+ </span>
+ <? if ($this->required) : ?>
+ <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+ <? endif ?>
+ </label>
+ <textarea name="<?= htmlReady($name) ?>"
+ v-model="<?= htmlReady($name) ?>"
+ id="<?= $id ?>"
+ <?= ($required ? 'required aria-required="true"' : '') ?>
+ <?= $attributes ?>><?= htmlReady($value) ?></textarea>
+</div>
diff --git a/templates/forms/user_filter_input.php b/templates/forms/user_filter_input.php
new file mode 100644
index 0000000..20d9386
--- /dev/null
+++ b/templates/forms/user_filter_input.php
@@ -0,0 +1,19 @@
+<div class="formpart" data-form-input-for="<?= htmlReady($name) ?>">
+ <label<?= $required ? ' class="studiprequired"' : '' ?> for="<?= htmlReady($id) ?>">
+ <span class="textlabel">
+ <?= htmlReady($title) ?>
+ </span>
+ <? if ($required) : ?>
+ <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span>
+ <? endif ?>
+ <user-filter-input
+ name="<?= htmlReady($name) ?>"
+ id="<?= htmlReady($id) ?>"
+ <?= $required ? 'required aria-required="true"' : '' ?>
+ value="<?= htmlReady($value) ?>"
+ v-model="<?= htmlReady($name) ?>"
+ <?= $attributes ?>
+ ></user-filter-input>
+ </label>
+
+</div>