From d1375e5f7b5d7543ec694df7c2f47b0a967f8951 Mon Sep 17 00:00:00 2001 From: Thomas Hackl Date: Mon, 25 Nov 2024 08:41:07 +0000 Subject: =?UTF-8?q?Resolve=20"Garuda=20in=20den=20Kern=20=C3=BCbernehmen"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3326 Merge request studip/studip!3035 --- app/controllers/admin/courses.php | 23 + app/controllers/massmail/message.php | 394 ++++++++++++++++ app/controllers/massmail/overview.php | 32 ++ app/controllers/massmail/permissions.php | 174 +++++++ app/controllers/massmail/quick.php | 90 ++++ app/controllers/massmail/settings.php | 109 +++++ app/views/admin/courses/massmail.php | 10 + db/migrations/6.0.32_integrate_garuda_plugin.php | 285 ++++++++++++ .../conditionaladmission/ConditionalAdmission.php | 2 + .../PreferentialAdmission.php | 1 + lib/classes/JsonApi/RouteMap.php | 9 + lib/classes/JsonApi/Routes/MassMail/Authority.php | 30 ++ .../Routes/MassMail/MassMailMessagesIndex.php | 71 +++ .../Routes/MassMail/MassMailPermissionsIndex.php | 31 ++ .../Routes/MassMail/MassMailPermissionsShow.php | 32 ++ .../JsonApi/Routes/UserFilters/Authority.php | 11 +- .../Routes/UserFilters/UserFilterFieldsIndex.php | 38 +- .../Routes/UserFilters/UserFiltersCreate.php | 8 +- .../Routes/UserFilters/UserFiltersDelete.php | 12 +- .../Routes/UserFilters/UserFiltersUpdate.php | 14 +- lib/classes/JsonApi/SchemaMap.php | 6 +- lib/classes/JsonApi/Schemas/Degree.php | 78 ++++ lib/classes/JsonApi/Schemas/MassMailMessage.php | 84 ++++ lib/classes/JsonApi/Schemas/MassMailPermission.php | 121 +++++ lib/classes/UserFilter.php | 320 +++++++++++++ lib/classes/UserFilterField.php | 517 +++++++++++++++++++++ .../UserFilterFields/DatafieldCondition.php | 166 +++++++ lib/classes/UserFilterFields/DegreeCondition.php | 54 +++ lib/classes/UserFilterFields/DomainCondition.php | 45 ++ .../MassMail/MassMailDegreeFilter.php | 126 +++++ .../MassMail/MassMailDomainFilter.php | 75 +++ .../MassMail/MassMailGenderFilter.php | 74 +++ .../MassMail/MassMailInstituteFilter.php | 140 ++++++ .../MassMail/MassMailPermissionFilter.php | 111 +++++ .../MassMailSelfAssignedInstituteFilter.php | 137 ++++++ .../MassMail/MassMailSemesterOfStudyFilter.php | 75 +++ .../MassMail/MassMailStatusgroupFilter.php | 88 ++++ .../MassMail/MassMailSubjectFilter.php | 125 +++++ .../UserFilterFields/PermissionCondition.php | 49 ++ .../UserFilterFields/SemesterOfStudyCondition.php | 84 ++++ .../UserFilterFields/StgteilVersionCondition.php | 86 ++++ lib/classes/UserFilterFields/SubjectCondition.php | 55 +++ .../UserFilterFields/SubjectConditionAny.php | 50 ++ lib/classes/UserFilterRange.php | 29 ++ lib/classes/admission/CourseSet.php | 46 +- lib/classes/admission/UserFilter.php | 280 ----------- lib/classes/admission/UserFilterField.php | 478 ------------------- .../admission/userfilter/DatafieldCondition.php | 164 ------- .../admission/userfilter/DegreeCondition.php | 51 -- .../admission/userfilter/PermissionCondition.php | 46 -- .../userfilter/SemesterOfStudyCondition.php | 81 ---- .../userfilter/StgteilVersionCondition.php | 83 ---- .../admission/userfilter/SubjectCondition.php | 52 --- .../admission/userfilter/SubjectConditionAny.php | 50 -- lib/classes/forms/CheckboxCollectionInput.php | 25 + lib/classes/forms/Fieldset.php | 17 + lib/classes/forms/FileInput.php | 23 + lib/classes/forms/Form.php | 20 +- lib/classes/forms/QuicksearchListInput.php | 19 + lib/classes/forms/SerialWysiwygInput.php | 34 ++ lib/classes/forms/UserFilterInput.php | 60 +++ lib/cronjobs/send_massmails.php | 107 +++++ lib/models/MassMail/MassMailFilter.php | 34 ++ lib/models/MassMail/MassMailMarker.php | 181 ++++++++ lib/models/MassMail/MassMailMessage.php | 373 +++++++++++++++ lib/models/MassMail/MassMailPermission.php | 139 ++++++ lib/models/MassMail/MassMailToken.php | 25 + lib/navigation/MessagingNavigation.php | 31 +- resources/vue/base-components.js | 4 + resources/vue/components/StudipUserFilter.vue | 20 +- resources/vue/components/StudipWysiwyg.vue | 2 + .../vue/components/form_inputs/FileUpload.vue | 198 ++++++++ .../form_inputs/QuicksearchListInput.vue | 97 ++++ .../components/form_inputs/SerialTextMarkers.vue | 80 ++++ .../vue/components/form_inputs/UserFilterInput.vue | 146 ++++++ .../components/massmail/MassMailMessagesList.vue | 153 ++++++ .../components/massmail/MassMailPermissions.vue | 108 +++++ templates/forms/checkbox_collection_input.php | 33 ++ templates/forms/fieldset.php | 10 +- templates/forms/file_input.php | 11 + templates/forms/form.php | 1 + templates/forms/quicksearchlist_input.php | 18 + templates/forms/radio_input.php | 16 +- templates/forms/serial_wysiwyg_input.php | 17 + templates/forms/textarea_input.php | 28 +- templates/forms/user_filter_input.php | 19 + 86 files changed, 6110 insertions(+), 1341 deletions(-) create mode 100644 app/controllers/massmail/message.php create mode 100644 app/controllers/massmail/overview.php create mode 100644 app/controllers/massmail/permissions.php create mode 100644 app/controllers/massmail/quick.php create mode 100644 app/controllers/massmail/settings.php create mode 100644 app/views/admin/courses/massmail.php create mode 100644 db/migrations/6.0.32_integrate_garuda_plugin.php create mode 100644 lib/classes/JsonApi/Routes/MassMail/Authority.php create mode 100644 lib/classes/JsonApi/Routes/MassMail/MassMailMessagesIndex.php create mode 100644 lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsIndex.php create mode 100644 lib/classes/JsonApi/Routes/MassMail/MassMailPermissionsShow.php create mode 100644 lib/classes/JsonApi/Schemas/Degree.php create mode 100644 lib/classes/JsonApi/Schemas/MassMailMessage.php create mode 100644 lib/classes/JsonApi/Schemas/MassMailPermission.php create mode 100644 lib/classes/UserFilter.php create mode 100644 lib/classes/UserFilterField.php create mode 100644 lib/classes/UserFilterFields/DatafieldCondition.php create mode 100644 lib/classes/UserFilterFields/DegreeCondition.php create mode 100644 lib/classes/UserFilterFields/DomainCondition.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailDegreeFilter.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailDomainFilter.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailGenderFilter.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailInstituteFilter.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailPermissionFilter.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailSelfAssignedInstituteFilter.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailSemesterOfStudyFilter.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailStatusgroupFilter.php create mode 100644 lib/classes/UserFilterFields/MassMail/MassMailSubjectFilter.php create mode 100644 lib/classes/UserFilterFields/PermissionCondition.php create mode 100644 lib/classes/UserFilterFields/SemesterOfStudyCondition.php create mode 100644 lib/classes/UserFilterFields/StgteilVersionCondition.php create mode 100644 lib/classes/UserFilterFields/SubjectCondition.php create mode 100644 lib/classes/UserFilterFields/SubjectConditionAny.php create mode 100644 lib/classes/UserFilterRange.php delete mode 100644 lib/classes/admission/UserFilter.php delete mode 100644 lib/classes/admission/UserFilterField.php delete mode 100644 lib/classes/admission/userfilter/DatafieldCondition.php delete mode 100644 lib/classes/admission/userfilter/DegreeCondition.php delete mode 100644 lib/classes/admission/userfilter/PermissionCondition.php delete mode 100644 lib/classes/admission/userfilter/SemesterOfStudyCondition.php delete mode 100644 lib/classes/admission/userfilter/StgteilVersionCondition.php delete mode 100644 lib/classes/admission/userfilter/SubjectCondition.php delete mode 100644 lib/classes/admission/userfilter/SubjectConditionAny.php create mode 100644 lib/classes/forms/CheckboxCollectionInput.php create mode 100644 lib/classes/forms/FileInput.php create mode 100644 lib/classes/forms/QuicksearchListInput.php create mode 100644 lib/classes/forms/SerialWysiwygInput.php create mode 100644 lib/classes/forms/UserFilterInput.php create mode 100644 lib/cronjobs/send_massmails.php create mode 100644 lib/models/MassMail/MassMailFilter.php create mode 100644 lib/models/MassMail/MassMailMarker.php create mode 100644 lib/models/MassMail/MassMailMessage.php create mode 100644 lib/models/MassMail/MassMailPermission.php create mode 100644 lib/models/MassMail/MassMailToken.php create mode 100644 resources/vue/components/form_inputs/FileUpload.vue create mode 100644 resources/vue/components/form_inputs/QuicksearchListInput.vue create mode 100644 resources/vue/components/form_inputs/SerialTextMarkers.vue create mode 100644 resources/vue/components/form_inputs/UserFilterInput.vue create mode 100644 resources/vue/components/massmail/MassMailMessagesList.vue create mode 100644 resources/vue/components/massmail/MassMailPermissions.vue create mode 100644 templates/forms/checkbox_collection_input.php create mode 100644 templates/forms/file_input.php create mode 100644 templates/forms/quicksearchlist_input.php create mode 100644 templates/forms/serial_wysiwyg_input.php create mode 100644 templates/forms/user_filter_input.php 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'] = ''; + $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 @@ +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('
' . $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('
' . $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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + + 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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/UserFilter.php b/lib/classes/UserFilter.php new file mode 100644 index 0000000..7745587 --- /dev/null +++ b/lib/classes/UserFilter.php @@ -0,0 +1,320 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +class UserFilter +{ + // --- ATTRIBUTES --- + + /** + * All condition fields that form this condition. + */ + public $fields = []; + + /** + * Unique identifier for this condition. + */ + public $id = ''; + + // Data about where this filter belongs. + public string $range_id = ''; + public string $range_type = ''; + + public $show_user_count = false; + + // --- OPERATIONS --- + + /** + * Standard constructor. + * + * @param String conditionId + * @return UserFilter + */ + public function __construct($conditionId = '') + { + $this->id = $conditionId; + if ($conditionId) { + $this->load(); + } else { + $this->id = $this->generateId(); + } + return $this; + } + + /** + * Add a new condition field. + * + * @param UserFilterField fieldId + * @return UserFilter + */ + public function addField($field) + { + $this->fields[$field->getId()] = $field; + $field->setConditionId($this->id); + return $this; + } + + /** + * Deletes the condition and all associated fields. + */ + public function delete() + { + // Delete condition data. + $stmt = DBManager::get()->prepare("DELETE FROM `userfilter` + WHERE `filter_id`=?"); + $stmt->execute([$this->id]); + // Delete all defined condition fields. + foreach ($this->fields as $field) { + $field->delete(); + } + } + + /** + * Generate a new unique ID. + * + * @param String tableName + */ + public function generateId() + { + do { + $newid = md5(uniqid(get_class($this) . microtime(), true)); + $id = DBManager::get()->fetchColumn("SELECT `filter_id` + FROM `userfilter` WHERE `filter_id`=?", [$newid]); + } while ($id); + return $newid; + } + + /** + * Get all fields (without checking for validity according + * to the current time). + * + * @return Array + */ + public function getFields() + { + uasort($this->fields, function ($a, $b) { + return $a->sortOrder - $b->sortOrder; + }); + return $this->fields; + } + + /** + * Get ID. + * + * @return String + */ + public function getId() + { + return $this->id; + } + + /** + * Gets all users that fulfill the current condition. + * + * @return Array + */ + public function getUsers() + { + $users = null; + foreach ($this->fields as $field) { + // Check if restrictions for the field value must be taken into consideration. + $restrictions = []; + foreach ($field->relations as $className => $related) { + if ($other = $this->hasField($className)) { + if ($other->getValue()) { + $restrictions[$className] = [ + 'table' => $other->userDataDbTable, + 'field' => $other->userDataDbField, + 'compare' => $other->getCompareOperator(), + 'value' => $other->getValue() + ]; + } + } + } + $users = isset($users) ? array_intersect($users, $field->getUsers($restrictions)) : $field->getUsers($restrictions); + } + return (array)$users; + } + + /** + * Checks whether the current filter object contains a field + * of the given type. + * + * @param String $className the type to check for + * @return UserFilterField Return the found field or null if not applicable. + */ + public function hasField($className) + { + foreach ($this->fields as $field) { + if ($field instanceof $className) { + return $field; + break; + } + } + return null; + } + + /** + * Is the current condition fulfilled (that means, are all + * required field values matched)? + * + * @return boolean + */ + public function isFulfilled($userId) + { + // Check all fields. + foreach ($this->fields as $field) { + if (!$field->checkValue($field->getUserValues($userId, $this->fields))) { + return false; + } + } + return true; + } + + /** + * Helper function for loading data from DB. + */ + 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` + WHERE `filter_id`=?"); + $stmt->execute([$this->id]); + while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { + /* + * Create instance of appropriate UserFilterField subclass. + * We just "try" here because the class definition could have + * 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']); + } + + $this->fields[$field->getId()] = $field; + //} catch (Exception $e) {} + } + } + } + + /** + * Removes the field with the given ID from the condition fields. + * + * @param String fieldId + * @return UserFilter + */ + public function removeField($fieldId) + { + unset($this->fields[$fieldId]); + return $this; + } + + /** + * Stores data to DB. + */ + public function store() + { + // Generate new ID if condition entry doesn't exist in DB yet. + if (!$this->id) { + $this->id = $this->generateId(); + } + + // Store condition data. + $stmt = DBManager::get()->prepare("INSERT INTO `userfilter` + (`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)) . "')"); + // Store all fields. + foreach ($this->fields as $field) { + $field->store($this->id); + } + } + + public function toString() + { + $tpl = $GLOBALS['template_factory']->open('userfilter/display'); + $tpl->set_attribute('filter', $this); + return $tpl->render(); + } + + public function __toString() + { + return $this->toString(); + } + + public function __clone() + { + $this->id = md5(uniqid(get_class($this))); + $cloned_fields = []; + foreach ($this->fields as $field) { + $dolly = clone $field; + $dolly->conditionId = $this->id; + $cloned_fields[$dolly->id] = $dolly; + } + $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/UserFilterField.php b/lib/classes/UserFilterField.php new file mode 100644 index 0000000..d997489 --- /dev/null +++ b/lib/classes/UserFilterField.php @@ -0,0 +1,517 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +class UserFilterField +{ + // --- ATTRIBUTES --- + + /** + * Which of the valid compare operators is currently chosen? + */ + public $compareOperator = ''; + + /** + * ID of the UserFilter this field belongs to. + */ + public $conditionId = ''; + + /** + * Unique ID for this condition field. + */ + public $id = ''; + + /** + * The set of valid compare operators. + */ + public $validCompareOperators = []; + + /** + * All valid values for this field. + */ + public $validValues = []; + + /** + * Which of the valid values is currently chosen? + */ + public $value = null; + + /* + * 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 static $sortOrder = 99; + + public static $isParameterized = false; + + protected static $cached_valid_values; + protected static $available_filter_fields; + + /** + * Database tables and fields to get valid values and concrete user values + * from. + */ + public $valuesDbTable = ''; + public $valuesDbIdField = ''; + public $valuesDbNameField = ''; + public $userDataDbTable = ''; + public $userDataDbField = ''; + public $relations = []; + + // --- OPERATIONS --- + + public static function getParameterizedTypes() + { + + } + + /** + * 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. + * + * @param String $fieldId If a fieldId is given, the corresponding data is + * loaded from database. + * + */ + public function __construct($fieldId = '') + { + $this->validCompareOperators = [ + '=' => _('ist'), + '!=' => _('ist nicht') + ]; + if ($this->valuesDbNameField) { + if (isset(self::$cached_valid_values[static::class])) { + $this->validValues = self::$cached_valid_values[static::class]; + } 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"); + while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $this->validValues[$current[$this->valuesDbIdField]] = $current[$this->valuesDbNameField]; + } + self::$cached_valid_values[static::class] = $this->validValues; + } + } + if ($fieldId) { + $this->id = $fieldId; + $this->load(); + } else { + $this->id = $this->generateId(); + } + } + + /** + * Checks whether the given value fits the configured condition. The + * value is compared to the currently selected value by using the + * currently selected compare operator. + * + * @param Array values + * @return Boolean + */ + public function checkValue($values) + { + // Validate compare operator + if (!isset($this->validCompareOperators[$this->compareOperator])) { + throw new Exception('Invalid compare operator'); + } + + $result = false; + foreach ($values as $value) { + switch ($this->compareOperator) { + case '=': + $result = $value == $this->value; + break; + case '!=': + $result = $value != $this->value; + break; + case '<': + $result = $value < $this->value; + break; + case '<=': + $result = $value <= $this->value; + break; + case '>=': + $result = $value >= $this->value; + break; + case '>': + $result = $value > $this->value; + break; + default: + throw new Exception('Unknown compare operator.'); + } + + if ($result) { + break; + } + } + return $result; + } + + /** + * Deletes the stored data for this condition field from DB. + */ + public function delete() + { + // Delete condition data. + $stmt = DBManager::get()->prepare("DELETE FROM `userfilter_fields` + WHERE `field_id`=?"); + $stmt->execute([$this->id]); + } + + /** + * Generate a new unique ID. + * + * @param String tableName + */ + public function generateId() + { + do { + $newid = md5(uniqid(get_class($this) . microtime(), true)); + $id = DBManager::get()->fetchColumn("SELECT `field_id` + FROM `userfilter_fields` WHERE `field_id`=?", [$newid]); + } while ($id); + return $newid; + } + + /** + * Reads all available UserFilterField subclasses and loads their definitions. + */ + public static function getAvailableFilterFields(string $context = '', string $target = '') + { + if (self::$available_filter_fields === null) { + $fields = []; + $i = new FileSystemIterator( + $GLOBALS['STUDIP_BASE_PATH'] . '/lib/classes/UserFilterFields' . ($context !== '' ? '/' . $context : ''), + FileSystemIterator::SKIP_DOTS + ); + + foreach ($i as $class) { + if ($class->isFile()) { + require_once $class; + } + } + + // Get all classes in given context. + $classes = array_filter( + get_declared_classes(), + 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()); + } else { + $filter = new $class(); + $fields[$class] = $filter->getName(); + } + } + self::$available_filter_fields = $fields; + } + return self::$available_filter_fields; + } + + + /** + * Which compare operator is set? + * + * @return String + */ + public function getCompareOperator() + { + return $this->compareOperator; + } + + /** + * Which compare operator is set? + * + * @return String + */ + public function getCompareOperatorAsText() + { + return $this->getValidCompareOperators()[$this->compareOperator] ?? ''; + } + + /** + * Field ID. + * + * @return String + */ + public function getId() + { + return $this->id; + } + + /** + * Get this field's display name. + * + * @return String + */ + public function getName() + { + return _("Nutzerfilterfeld"); + } + + /** + * Compares all the users' values by using the specified compare operator + * and returns all users that fulfill the condition. This can be + * an important information when checking on validity of a combination + * of conditions. + * + * @param Array $restrictions values from other fields that restrict the valid + * values for a user (e.g. a semester of study in + * a given subject) + * @return Array All users that are affected by the current condition + * field. + */ + public function getUsers($restrictions = []) + { + $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 . "?"; + $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']; + } + } + // Get all the users that fulfill the condition. + $stmt = $db->prepare($select . $from . $where); + $stmt->execute($parameters); + while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $users[] = $current['user_id']; + } + 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) + { + $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; + } + + /** + * Returns all valid compare operators. + * + * @return Array Array of valid compare operators. + */ + public function getValidCompareOperators() + { + return $this->validCompareOperators; + } + + /** + * Returns all valid values. Values can be loaded dynamically from + * database or be returned as static array. + * + * @return Array Valid values in the form $value => $displayname. + */ + public function getValidValues() + { + return $this->validValues; + } + + /** + * Which value is set? + * + * @return String + */ + public function getValue() + { + return $this->value; + } + + /** + * Helper function for loading data from DB. + */ + public function load() + { + $stmt = DBManager::get()->prepare( + "SELECT * FROM `userfilter_fields` WHERE `field_id`=? LIMIT 1"); + $stmt->execute([$this->id]); + if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { + $this->conditionId = $data['filter_id']; + $this->value = $data['value']; + $this->compareOperator = $data['compare_op']; + } + } + + /** + * Sets a new selected compare operator + * + * @param String newOperator + * @return UserFilterField + */ + public function setCompareOperator($newOperator) + { + if (in_array($newOperator, array_keys($this->validCompareOperators))) { + $this->compareOperator = $newOperator; + return $this; + } else { + return false; + } + } + + /** + * Connects the current field to a UserFilter. + * + * @param String $id ID of a UserFilter object. + * @return UserFilterField + */ + public function setConditionId($id) + { + $this->conditionId = $id; + return $this; + } + + /** + * Sets a new selected value. + * + * @param String newValue + * @return UserFilterField + */ + public function setValue($newValue) + { + if ($this->validValues[$newValue]) { + $this->value = $newValue; + return $this; + } else { + return false; + } + } + + /** + * Stores data to DB. + * + * @param String conditionId The condition this field belongs to. + */ + public function store() + { + // Generate new ID if field entry doesn't exist in DB yet. + if (!$this->id) { + $this->id = $this->generateId(); + } + // Store field data. + $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`), + `type`=VALUES(`type`),`value`=VALUES(`value`), + `compare_op`=VALUES(`compare_op`), `chdate`=VALUES(`chdate`)"); + $stmt->execute([$this->id, $this->conditionId, get_class($this), + $this->value, $this->compareOperator, time(), time()]); + } + + public function __clone() + { + $this->id = md5(uniqid(get_class($this))); + $this->conditionId = null; + } + +} /* end of class UserFilterField */ diff --git a/lib/classes/UserFilterFields/DatafieldCondition.php b/lib/classes/UserFilterFields/DatafieldCondition.php new file mode 100644 index 0000000..9e7a2c3 --- /dev/null +++ b/lib/classes/UserFilterFields/DatafieldCondition.php @@ -0,0 +1,166 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +namespace UserFilterFields; + +class DatafieldCondition extends \UserFilterField +{ + public static $isParameterized = true; + + public $datafield_id, $null_yields, $datafield_name; + + 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) { + $ret[__CLASS__ . '_' . $df->id] = utf8_encode(chr(160)) . _("Datenfeld") . ': ' . $df->name; + } + } catch (\PDOException $e) {} //migration 128 chokes on this... + return $ret; + } + /** + * @see UserFilterField::__construct + */ + public function __construct($typeparam, $fieldId = '') + { + $this->validCompareOperators = [ + '>=' => _('mindestens'), + '<=' => _('höchstens'), + '=' => _('ist'), + '!=' => _('ist nicht') + ]; + if ($fieldId) { + $this->id = $fieldId; + $this->load(); + } else { + $this->id = $this->generateId(); + $this->datafield_id = $typeparam; + } + + $df = \DataField::find($this->datafield_id); + if ($df) { + $this->datafield_name = $df->name; + } else { + throw new \UnexpectedValueException('datafield not found, id: ' . $typeparam); + } + $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) { + 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); + } else { + $this->null_yields = ''; + } + + } + + /** + * Get this field's display name. + * + * @return String + */ + public function getName() + { + return $this->datafield_name; + } + + public function getUsers($restrictions = []) + { + $db = \DBManager::get(); + // Standard query getting the values without respecting other values. + $select = "SELECT user_id FROM + auth_user_md5 LEFT JOIN + datafields_entries ON range_id = user_id AND datafield_id = ? + WHERE perms IN ('user','autor','tutor','dozent') AND IFNULL(content, ?) + " . $this->compareOperator . " ?"; + $users = $db->fetchFirst($select, [$this->datafield_id, $this->null_yields,$this->value]); + return $users; + } + + /** + * Gets the value for the given user that is relevant for this + * + * @param String $userId User to check. + * @param Array $additional additional conditions that are required for check. + * @return array The value(s) for this user. + */ + public function getUserValues($userId, $additional = null) + { + $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]; + } + + /** + * Helper function for loading data from DB. + */ + public function load() + { + $stmt = \DBManager::get()->prepare( + "SELECT * FROM `userfilter_fields` WHERE `field_id`=? LIMIT 1"); + $stmt->execute([$this->id]); + if ($data = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $this->conditionId = $data['filter_id']; + $this->value = $data['value']; + $this->compareOperator = $data['compare_op']; + list(,$this->datafield_id) = explode('_', $data['type']); + } + } + + /** + * Sets a new selected value. + * + * @param String newValue + * @return UserFilterField + */ + public function setValue($newValue) + { + $this->value = $newValue; + return $this; + } + + /** + * Stores data to DB. + * + */ + public function store() + { + // Generate new ID if field entry doesn't exist in DB yet. + if (!$this->id) { + $this->id = $this->generateId(); + } + // Store field data. + $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`), + `type`=VALUES(`type`),`value`=VALUES(`value`), + `compare_op`=VALUES(`compare_op`), `chdate`=VALUES(`chdate`)"); + $stmt->execute([$this->id, $this->conditionId, get_class($this).'_'.$this->datafield_id, + $this->value, $this->compareOperator, time(), time()]); + } +} diff --git a/lib/classes/UserFilterFields/DegreeCondition.php b/lib/classes/UserFilterFields/DegreeCondition.php new file mode 100644 index 0000000..6b26fb0 --- /dev/null +++ b/lib/classes/UserFilterFields/DegreeCondition.php @@ -0,0 +1,54 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +namespace UserFilterFields; + +class DegreeCondition extends \UserFilterField +{ + // --- ATTRIBUTES --- + public $valuesDbTable = 'abschluss'; + public $valuesDbIdField = 'abschluss_id'; + public $valuesDbNameField = 'name'; + public $userDataDbTable = 'user_studiengang'; + public $userDataDbField = 'abschluss_id'; + + public static $sortOrder = 1; + + /** + * @see UserFilterField::__construct + */ + public function __construct($fieldId = '') + { + parent::__construct($fieldId); + $this->relations = [ + 'SubjectCondition' => [ + 'local_field' => 'fach_id', + 'foreign_field' => 'fach_id' + ] + ]; + } + + /** + * Get this field's display name. + * + * @return String + */ + public function getName() + { + return _('Abschluss'); + } + +} 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 @@ + + * @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 @@ +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 @@ + + * @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 @@ +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 @@ +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 @@ + + * @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 @@ +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 @@ +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 @@ +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 @@ +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/UserFilterFields/PermissionCondition.php b/lib/classes/UserFilterFields/PermissionCondition.php new file mode 100644 index 0000000..10212c7 --- /dev/null +++ b/lib/classes/UserFilterFields/PermissionCondition.php @@ -0,0 +1,49 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +namespace UserFilterFields; + +class PermissionCondition extends \UserFilterField +{ + public static $sortOrder = 7; + + /** + * @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'); + } +} diff --git a/lib/classes/UserFilterFields/SemesterOfStudyCondition.php b/lib/classes/UserFilterFields/SemesterOfStudyCondition.php new file mode 100644 index 0000000..f66789f --- /dev/null +++ b/lib/classes/UserFilterFields/SemesterOfStudyCondition.php @@ -0,0 +1,84 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +namespace UserFilterFields; + +class SemesterOfStudyCondition extends \UserFilterField +{ + // --- ATTRIBUTES --- + public $valuesDbTable = 'user_studiengang'; + public $valuesDbIdField = 'semester'; + public $userDataDbTable = 'user_studiengang'; + public $userDataDbField = 'semester'; + + public static $sortOrder = 4; + + // --- OPERATIONS --- + + /** + * @see UserFilterField::__construct + */ + public function __construct($fieldId='') + { + parent::__construct($fieldId); + $this->validValues = []; + $this->relations = [ + 'DegreeCondition' => [ + 'local_field' => 'abschluss_id', + 'foreign_field' => 'abschluss_id' + ], + 'SubjectCondition' => [ + '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/StgteilVersionCondition.php b/lib/classes/UserFilterFields/StgteilVersionCondition.php new file mode 100644 index 0000000..59bb035 --- /dev/null +++ b/lib/classes/UserFilterFields/StgteilVersionCondition.php @@ -0,0 +1,86 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +namespace UserFilterFields; + +class StgteilVersionCondition extends \UserFilterField +{ + // --- ATTRIBUTES --- + public $valuesDbTable = 'mvv_stgteilversion'; + public $valuesDbIdField = 'version_id'; + public $valuesDbNameField = 'code'; + public $userDataDbTable = 'user_studiengang'; + public $userDataDbField = 'version_id'; + + public static $sortOrder = 5; + + public static $isParameterized = true; + + public static function getParameterizedTypes() + { + if (\Config::get()->DISPLAY_STGTEILVERSION_USERFILTER) { + $filter = new StgteilVersionCondition(); + $fields['StgteilVersionCondition'] = $filter->getName(); + return $fields; + } else { + return []; + } + } + + /** + * @see UserFilterField::__construct + */ + public function __construct($fieldId = '') + { + $this->validCompareOperators = [ + '=' => _('ist'), + '!=' => _('ist nicht') + ]; + if ($this->valuesDbNameField) { + // Get all available values from database. + $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)) { + $this->validValues[$current[$this->valuesDbIdField]] = $current[$this->valuesDbNameField]; + } + } + if ($fieldId) { + $this->id = $fieldId; + $this->load(); + } else { + $this->id = $this->generateId(); + } + + foreach ($this->validValues as $version_id => $name) { + $stgteilversion = \StgteilVersion::find($version_id); + $this->validValues[$version_id] = $stgteilversion->getDisplayName(); + } + } + + /** + * Get this field's display name. + * + * @return String + */ + public function getName() + { + return _('Studiengangteil-Version'); + } +} diff --git a/lib/classes/UserFilterFields/SubjectCondition.php b/lib/classes/UserFilterFields/SubjectCondition.php new file mode 100644 index 0000000..e9ac1a0 --- /dev/null +++ b/lib/classes/UserFilterFields/SubjectCondition.php @@ -0,0 +1,55 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ +namespace UserFilterFields; + +class SubjectCondition extends \UserFilterField +{ + // --- ATTRIBUTES --- + public $valuesDbTable = 'fach'; + public $valuesDbIdField = 'fach_id'; + public $valuesDbNameField = 'name'; + public $userDataDbTable = 'user_studiengang'; + public $userDataDbField = 'fach_id'; + + public static $sortOrder = 2; + + // --- OPERATIONS --- + + /** + * @see UserFilterField::__construct + */ + public function __construct($fieldId = '') + { + parent::__construct($fieldId); + $this->relations = [ + 'DegreeCondition' => [ + 'local_field' => 'abschluss_id', + 'foreign_field' => 'abschluss_id' + ] + ]; + } + + /** + * Get this field's display name. + * + * @return String + */ + public function getName() + { + return _('Studienfach'); + } +} diff --git a/lib/classes/UserFilterFields/SubjectConditionAny.php b/lib/classes/UserFilterFields/SubjectConditionAny.php new file mode 100644 index 0000000..c99bcb8 --- /dev/null +++ b/lib/classes/UserFilterFields/SubjectConditionAny.php @@ -0,0 +1,50 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ + +namespace UserFilterFields; + +class SubjectConditionAny extends \UserFilterField +{ + // --- ATTRIBUTES --- + public $userDataDbTable = 'user_studiengang'; + public $userDataDbField = 'fach_id'; + + public static $sortOrder = 3; + + // --- OPERATIONS --- + + /** + * @see UserFilterField::__construct + */ + public function __construct($fieldId = '') + { + parent::__construct($fieldId); + $this->validCompareOperators = [ + '!=' => ' ' + ]; + $this->validValues = ['' => ' ']; + } + + /** + * Get this field's display name. + * + * @return String + */ + public function getName() + { + return _('Alle Studienfächer'); + } +} 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 @@ + + * @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/admission/UserFilter.php b/lib/classes/admission/UserFilter.php deleted file mode 100644 index fd160d6..0000000 --- a/lib/classes/admission/UserFilter.php +++ /dev/null @@ -1,280 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ - -class UserFilter -{ - // --- ATTRIBUTES --- - - /** - * All condition fields that form this condition. - */ - public $fields = []; - - /** - * Unique identifier for this condition. - */ - public $id = ''; - - public $show_user_count = false; - - // --- OPERATIONS --- - - /** - * Standard constructor. - * - * @param String conditionId - * @return UserFilter - */ - public function __construct($conditionId='') - { - UserFilterField::getAvailableFilterFields(); - $this->id = $conditionId; - if ($conditionId) { - $this->load(); - } else { - $this->id = $this->generateId(); - } - return $this; - } - - /** - * Add a new condition field. - * - * @param ConditionField fieldId - * @return UserFilter - */ - public function addField($field) - { - $this->fields[$field->getId()] = $field; - $field->setConditionId($this->id); - return $this; - } - - /** - * Deletes the condition and all associated fields. - */ - public function delete() { - // Delete condition data. - $stmt = DBManager::get()->prepare("DELETE FROM `userfilter` - WHERE `filter_id`=?"); - $stmt->execute([$this->id]); - // Delete all defined condition fields. - foreach ($this->fields as $field) { - $field->delete(); - } - } - - /** - * Generate a new unique ID. - * - * @param String tableName - */ - public function generateId() { - do { - $newid = md5(uniqid(get_class($this).microtime(), true)); - $id = DBManager::get()->fetchColumn("SELECT `filter_id` - FROM `userfilter` WHERE `filter_id`=?", [$newid]); - } while ($id); - return $newid; - } - - /** - * Get all fields (without checking for validity according - * to the current time). - * - * @return Array - */ - public function getFields() - { - uasort($this->fields, function($a, $b) { - return $a->sortOrder - $b->sortOrder; - }); - return $this->fields; - } - - /** - * Get ID. - * - * @return String - */ - public function getId() - { - return $this->id; - } - - /** - * Gets all users that fulfill the current condition. - * - * @return Array - */ - public function getUsers() { - $users = null; - foreach ($this->fields as $field) { - // Check if restrictions for the field value must be taken into consideration. - $restrictions = []; - foreach ($field->relations as $className => $related) { - if ($other = $this->hasField($className)) { - if ($other->getValue()) { - $restrictions[$className] = [ - 'table' => $other->userDataDbTable, - 'field' => $other->userDataDbField, - 'compare' => $other->getCompareOperator(), - 'value' => $other->getValue() - ]; - } - } - } - $users = isset($users) ? array_intersect($users, $field->getUsers($restrictions)) : $field->getUsers($restrictions); - } - return (array) $users; - } - - /** - * Checks whether the current filter object contains a field - * of the given type. - * - * @param String $className the type to check for - * @return UserFilterField Return the found field or null if not applicable. - */ - public function hasField($className) { - foreach ($this->fields as $field) { - if ($field instanceof $className) { - return $field; - break; - } - } - return null; - } - - /** - * Is the current condition fulfilled (that means, are all - * required field values matched)? - * - * @return boolean - */ - public function isFulfilled($userId) - { - // Check all fields. - foreach ($this->fields as $field) { - if (!$field->checkValue($field->getUserValues($userId, $this->fields))) { - return false; - } - } - return true; - } - - /** - * Helper function for loading data from DB. - */ - 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']; - // Load the associated condition fields. - $stmt = DBManager::get()->prepare( - "SELECT `field_id`, `type` FROM `userfilter_fields` - WHERE `filter_id`=?"); - $stmt->execute([$this->id]); - while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - /* - * Create instance of appropriate UserFilterField subclass. - * We just "try" here because the class definition could have - * 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']); - } - - $this->fields[$field->getId()] = $field; - //} catch (Exception $e) {} - } - } - } - - /** - * Removes the field with the given ID from the condition fields. - * - * @param String fieldId - * @return UserFilter - */ - public function removeField($fieldId) - { - unset($this->fields[$fieldId]); - return $this; - } - - /** - * Stores data to DB. - */ - public function store() { - // Generate new ID if condition entry doesn't exist in DB yet. - if (!$this->id) { - $this->id = $this->generateId(); - } - - // 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()]); - // 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))."')"); - // Store all fields. - foreach ($this->fields as $field) { - $field->store($this->id); - } - } - - public function toString() { - $tpl = $GLOBALS['template_factory']->open('userfilter/display'); - $tpl->set_attribute('filter', $this); - return $tpl->render(); - } - - public function __toString() { - return $this->toString(); - } - - public function __clone() - { - $this->id = md5(uniqid(get_class($this))); - $cloned_fields= []; - foreach ($this->fields as $field) { - $dolly = clone $field; - $dolly->conditionId = $this->id; - $cloned_fields[$dolly->id] = $dolly; - } - $this->fields = $cloned_fields; - } - -} /* end of class UserFilter */ - -?> diff --git a/lib/classes/admission/UserFilterField.php b/lib/classes/admission/UserFilterField.php deleted file mode 100644 index 2a34807..0000000 --- a/lib/classes/admission/UserFilterField.php +++ /dev/null @@ -1,478 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ - -class UserFilterField -{ - // --- ATTRIBUTES --- - - /** - * Which of the valid compare operators is currently chosen? - */ - public $compareOperator = ''; - - /** - * ID of the UserFilter this field belongs to. - */ - public $conditionId = ''; - - /** - * Unique ID for this condition field. - */ - public $id = ''; - - /** - * The set of valid compare operators. - */ - public $validCompareOperators = []; - - /** - * All valid values for this field. - */ - public $validValues = []; - - /** - * Which of the valid values is currently chosen? - */ - public $value = null; - - /* - * 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 $isParameterized = false; - - protected static $cached_valid_values; - protected static $available_filter_fields; - - /** - * Database tables and fields to get valid values and concrete user values - * from. - */ - public $valuesDbTable = ''; - public $valuesDbIdField = ''; - public $valuesDbNameField = ''; - public $userDataDbTable = ''; - public $userDataDbField = ''; - public $relations = []; - - // --- OPERATIONS --- - - public static function getParameterizedTypes() - { - - } - - - /** - * Standard constructor. - * - * @param String $fieldId If a fieldId is given, the corresponding data is - * loaded from database. - * - */ - public function __construct($fieldId = '') - { - $this->validCompareOperators = [ - '=' => _('ist'), - '!=' => _('ist nicht') - ]; - if ($this->valuesDbNameField) { - if (isset(self::$cached_valid_values[static::class])) { - $this->validValues = self::$cached_valid_values[static::class]; - } 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"); - while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { - $this->validValues[$current[$this->valuesDbIdField]] = $current[$this->valuesDbNameField]; - } - self::$cached_valid_values[static::class] = $this->validValues; - } - } - if ($fieldId) { - $this->id = $fieldId; - $this->load(); - } else { - $this->id = $this->generateId(); - } - } - - /** - * Checks whether the given value fits the configured condition. The - * value is compared to the currently selected value by using the - * currently selected compare operator. - * - * @param Array values - * @return Boolean - */ - public function checkValue($values) - { - // Validate compare operator - if (!isset($this->validCompareOperators[$this->compareOperator])) { - throw new Exception('Invalid compare operator'); - } - - $result = false; - foreach ($values as $value) { - switch ($this->compareOperator) { - case '=': - $result = $value == $this->value; - break; - case '!=': - $result = $value != $this->value; - break; - case '<': - $result = $value < $this->value; - break; - case '<=': - $result = $value <= $this->value; - break; - case '>=': - $result = $value >= $this->value; - break; - case '>': - $result = $value > $this->value; - break; - default: - throw new Exception('Unknown compare operator.'); - } - - if ($result) { - break; - } - } - return $result; - } - - /** - * Deletes the stored data for this condition field from DB. - */ - public function delete() - { - // Delete condition data. - $stmt = DBManager::get()->prepare("DELETE FROM `userfilter_fields` - WHERE `field_id`=?"); - $stmt->execute([$this->id]); - } - - /** - * Generate a new unique ID. - * - * @param String tableName - */ - public function generateId() - { - do { - $newid = md5(uniqid(get_class($this).microtime(), true)); - $id = DBManager::get()->fetchColumn("SELECT `field_id` - FROM `userfilter_fields` WHERE `field_id`=?", [$newid]); - } while ($id); - return $newid; - } - - /** - * Reads all available UserFilterField subclasses and loads their definitions. - */ - public static function getAvailableFilterFields() - { - if (self::$available_filter_fields === null) { - $fields = []; - $i = new FileSystemIterator( - $GLOBALS['STUDIP_BASE_PATH'] . '/lib/classes/admission/userfilter', - FileSystemIterator::SKIP_DOTS - ); - - foreach ($i as $class) { - require_once $class; - } - - $classes = array_filter( - get_declared_classes(), - fn($c) => is_subclass_of($c, UserFilterField::class) - ); - foreach ($classes as $class) { - if ($class::$isParameterized) { - $fields = array_merge($fields, $class::getParameterizedTypes()); - } else { - $filter = new $class(); - $fields[$class] = $filter->getName(); - } - } - asort($fields); - self::$available_filter_fields = $fields; - } - return self::$available_filter_fields; - } - - - /** - * Which compare operator is set? - * - * @return String - */ - public function getCompareOperator() - { - return $this->compareOperator; - } - - /** - * Which compare operator is set? - * - * @return String - */ - public function getCompareOperatorAsText() - { - return $this->getValidCompareOperators()[$this->compareOperator] ?? ''; - } - - /** - * Field ID. - * - * @return String - */ - public function getId() - { - return $this->id; - } - - /** - * Get this field's display name. - * - * @return String - */ - public function getName() - { - return _("Nutzerfilterfeld"); - } - - /** - * Compares all the users' values by using the specified compare operator - * and returns all users that fulfill the condition. This can be - * an important information when checking on validity of a combination - * of conditions. - * - * @param Array $restrictions values from other fields that restrict the valid - * values for a user (e.g. a semester of study in - * a given subject) - * @return Array All users that are affected by the current condition - * field. - */ - public function getUsers($restrictions = []) - { - $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."?"; - $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']; - } - } - // Get all the users that fulfill the condition. - $stmt = $db->prepare($select.$from.$where); - $stmt->execute($parameters); - while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { - $users[] = $current['user_id']; - } - 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) - { - $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; - } - - /** - * Returns all valid compare operators. - * - * @return Array Array of valid compare operators. - */ - public function getValidCompareOperators() - { - return $this->validCompareOperators; - } - - /** - * Returns all valid values. Values can be loaded dynamically from - * database or be returned as static array. - * - * @return Array Valid values in the form $value => $displayname. - */ - public function getValidValues() - { - return $this->validValues; - } - - /** - * Which value is set? - * - * @return String - */ - public function getValue() - { - return $this->value; - } - - /** - * Helper function for loading data from DB. - */ - public function load() - { - $stmt = DBManager::get()->prepare( - "SELECT * FROM `userfilter_fields` WHERE `field_id`=? LIMIT 1"); - $stmt->execute([$this->id]); - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - $this->conditionId = $data['filter_id']; - $this->value = $data['value']; - $this->compareOperator = $data['compare_op']; - } - } - - /** - * Sets a new selected compare operator - * - * @param String newOperator - * @return UserFilterField - */ - public function setCompareOperator($newOperator) - { - if (in_array($newOperator, array_keys($this->validCompareOperators))) { - $this->compareOperator = $newOperator; - return $this; - } else { - return false; - } - } - - /** - * Connects the current field to a UserFilter. - * - * @param String $id ID of a UserFilter object. - * @return UserFilterField - */ - public function setConditionId($id) - { - $this->conditionId = $id; - return $this; - } - - /** - * Sets a new selected value. - * - * @param String newValue - * @return UserFilterField - */ - public function setValue($newValue) - { - if ($this->validValues[$newValue]) { - $this->value = $newValue; - return $this; - } else { - return false; - } - } - - /** - * Stores data to DB. - * - * @param String conditionId The condition this field belongs to. - */ - public function store() - { - // Generate new ID if field entry doesn't exist in DB yet. - if (!$this->id) { - $this->id = $this->generateId(); - } - // Store field data. - $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`), - `type`=VALUES(`type`),`value`=VALUES(`value`), - `compare_op`=VALUES(`compare_op`), `chdate`=VALUES(`chdate`)"); - $stmt->execute([$this->id, $this->conditionId, get_class($this), - $this->value, $this->compareOperator, time(), time()]); - } - - public function __clone() - { - $this->id = md5(uniqid(get_class($this))); - $this->conditionId = null; - } - -} /* end of class UserFilterField */ diff --git a/lib/classes/admission/userfilter/DatafieldCondition.php b/lib/classes/admission/userfilter/DatafieldCondition.php deleted file mode 100644 index 1bc93e8..0000000 --- a/lib/classes/admission/userfilter/DatafieldCondition.php +++ /dev/null @@ -1,164 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ -class DatafieldCondition extends UserFilterField -{ - public static $isParameterized = true; - - public $datafield_id, $null_yields, $datafield_name; - - public $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) { - $ret[__CLASS__ . '_' . $df->id] = utf8_encode(chr(160)) . _("Datenfeld") . ': ' . $df->name; - } - } catch (PDOException $e) {} //migration 128 chokes on this... - return $ret; - } - /** - * @see UserFilterField::__construct - */ - public function __construct($typeparam, $fieldId = '') - { - $this->validCompareOperators = [ - '>=' => _('mindestens'), - '<=' => _('höchstens'), - '=' => _('ist'), - '!=' => _('ist nicht') - ]; - if ($fieldId) { - $this->id = $fieldId; - $this->load(); - } else { - $this->id = $this->generateId(); - $this->datafield_id = $typeparam; - } - - $df = DataField::find($this->datafield_id); - if ($df) { - $this->datafield_name = $df->name; - } else { - throw new UnexpectedValueException('datafield not found, id: ' . $typeparam); - } - $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) { - 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); - } else { - $this->null_yields = ''; - } - - } - - /** - * Get this field's display name. - * - * @return String - */ - public function getName() - { - return $this->datafield_name; - } - - public function getUsers($restrictions = []) - { - $db = DBManager::get(); - // Standard query getting the values without respecting other values. - $select = "SELECT user_id FROM - auth_user_md5 LEFT JOIN - datafields_entries ON range_id = user_id AND datafield_id = ? - WHERE perms IN ('user','autor','tutor','dozent') AND IFNULL(content, ?) - " . $this->compareOperator . " ?"; - $users = $db->fetchFirst($select, [$this->datafield_id, $this->null_yields,$this->value]); - return $users; - } - - /** - * Gets the value for the given user that is relevant for this - * - * @param String $userId User to check. - * @param Array $additional additional conditions that are required for check. - * @return array The value(s) for this user. - */ - public function getUserValues($userId, $additional = null) - { - $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]; - } - - /** - * Helper function for loading data from DB. - */ - public function load() - { - $stmt = DBManager::get()->prepare( - "SELECT * FROM `userfilter_fields` WHERE `field_id`=? LIMIT 1"); - $stmt->execute([$this->id]); - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - $this->conditionId = $data['filter_id']; - $this->value = $data['value']; - $this->compareOperator = $data['compare_op']; - list(,$this->datafield_id) = explode('_', $data['type']); - } - } - - /** - * Sets a new selected value. - * - * @param String newValue - * @return UserFilterField - */ - public function setValue($newValue) - { - $this->value = $newValue; - return $this; - } - - /** - * Stores data to DB. - * - */ - public function store() - { - // Generate new ID if field entry doesn't exist in DB yet. - if (!$this->id) { - $this->id = $this->generateId(); - } - // Store field data. - $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`), - `type`=VALUES(`type`),`value`=VALUES(`value`), - `compare_op`=VALUES(`compare_op`), `chdate`=VALUES(`chdate`)"); - $stmt->execute([$this->id, $this->conditionId, get_class($this).'_'.$this->datafield_id, - $this->value, $this->compareOperator, time(), time()]); - } -} diff --git a/lib/classes/admission/userfilter/DegreeCondition.php b/lib/classes/admission/userfilter/DegreeCondition.php deleted file mode 100644 index 61ce456..0000000 --- a/lib/classes/admission/userfilter/DegreeCondition.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ -class DegreeCondition extends UserFilterField -{ - // --- ATTRIBUTES --- - public $valuesDbTable = 'abschluss'; - public $valuesDbIdField = 'abschluss_id'; - public $valuesDbNameField = 'name'; - public $userDataDbTable = 'user_studiengang'; - public $userDataDbField = 'abschluss_id'; - - public $sortOrder = 1; - - /** - * @see UserFilterField::__construct - */ - public function __construct($fieldId = '') - { - parent::__construct($fieldId); - $this->relations = [ - 'SubjectCondition' => [ - 'local_field' => 'fach_id', - 'foreign_field' => 'fach_id' - ] - ]; - } - - /** - * Get this field's display name. - * - * @return String - */ - public function getName() - { - return _('Abschluss'); - } - -} diff --git a/lib/classes/admission/userfilter/PermissionCondition.php b/lib/classes/admission/userfilter/PermissionCondition.php deleted file mode 100644 index fe9458c..0000000 --- a/lib/classes/admission/userfilter/PermissionCondition.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ -class PermissionCondition extends UserFilterField -{ - public $sortOrder = 7; - - /** - * @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'); - } -} diff --git a/lib/classes/admission/userfilter/SemesterOfStudyCondition.php b/lib/classes/admission/userfilter/SemesterOfStudyCondition.php deleted file mode 100644 index 5794f75..0000000 --- a/lib/classes/admission/userfilter/SemesterOfStudyCondition.php +++ /dev/null @@ -1,81 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ -class SemesterOfStudyCondition extends UserFilterField -{ - // --- ATTRIBUTES --- - public $valuesDbTable = 'user_studiengang'; - public $valuesDbIdField = 'semester'; - public $userDataDbTable = 'user_studiengang'; - public $userDataDbField = 'semester'; - - public $sortOrder = 4; - - // --- OPERATIONS --- - - /** - * @see UserFilterField::__construct - */ - public function __construct($fieldId='') - { - parent::__construct($fieldId); - $this->validValues = []; - $this->relations = [ - 'DegreeCondition' => [ - 'local_field' => 'abschluss_id', - 'foreign_field' => 'abschluss_id' - ], - 'SubjectCondition' => [ - '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/admission/userfilter/StgteilVersionCondition.php b/lib/classes/admission/userfilter/StgteilVersionCondition.php deleted file mode 100644 index ec5c1f3..0000000 --- a/lib/classes/admission/userfilter/StgteilVersionCondition.php +++ /dev/null @@ -1,83 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ -class StgteilVersionCondition extends UserFilterField -{ - // --- ATTRIBUTES --- - public $valuesDbTable = 'mvv_stgteilversion'; - public $valuesDbIdField = 'version_id'; - public $valuesDbNameField = 'code'; - public $userDataDbTable = 'user_studiengang'; - public $userDataDbField = 'version_id'; - - public $sortOrder = 5; - - public static $isParameterized = true; - - public static function getParameterizedTypes() - { - if (Config::get()->DISPLAY_STGTEILVERSION_USERFILTER) { - $filter = new StgteilVersionCondition; - $fields['StgteilVersionCondition'] = $filter->getName(); - return $fields; - } else { - return []; - } - } - - /** - * @see UserFilterField::__construct - */ - public function __construct($fieldId = '') - { - $this->validCompareOperators = [ - '=' => _('ist'), - '!=' => _('ist nicht') - ]; - if ($this->valuesDbNameField) { - // Get all available values from database. - $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)) { - $this->validValues[$current[$this->valuesDbIdField]] = $current[$this->valuesDbNameField]; - } - } - if ($fieldId) { - $this->id = $fieldId; - $this->load(); - } else { - $this->id = $this->generateId(); - } - - foreach ($this->validValues as $version_id => $name) { - $stgteilversion = StgteilVersion::find($version_id); - $this->validValues[$version_id] = $stgteilversion->getDisplayName(); - } - } - - /** - * Get this field's display name. - * - * @return String - */ - public function getName() - { - return _('Studiengangteil-Version'); - } -} diff --git a/lib/classes/admission/userfilter/SubjectCondition.php b/lib/classes/admission/userfilter/SubjectCondition.php deleted file mode 100644 index 7aa5f26..0000000 --- a/lib/classes/admission/userfilter/SubjectCondition.php +++ /dev/null @@ -1,52 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ -class SubjectCondition extends UserFilterField -{ - // --- ATTRIBUTES --- - public $valuesDbTable = 'fach'; - public $valuesDbIdField = 'fach_id'; - public $valuesDbNameField = 'name'; - public $userDataDbTable = 'user_studiengang'; - public $userDataDbField = 'fach_id'; - - public $sortOrder = 2; - - // --- OPERATIONS --- - - /** - * @see UserFilterField::__construct - */ - public function __construct($fieldId = '') - { - parent::__construct($fieldId); - $this->relations = [ - 'DegreeCondition' => [ - 'local_field' => 'abschluss_id', - 'foreign_field' => 'abschluss_id' - ] - ]; - } - - /** - * Get this field's display name. - * - * @return String - */ - public function getName() - { - return _('Studienfach'); - } -} diff --git a/lib/classes/admission/userfilter/SubjectConditionAny.php b/lib/classes/admission/userfilter/SubjectConditionAny.php deleted file mode 100644 index 3a3712b..0000000 --- a/lib/classes/admission/userfilter/SubjectConditionAny.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ - -require_once realpath(__DIR__ . '/..') . '/UserFilterField.php'; - -class SubjectConditionAny extends UserFilterField -{ - // --- ATTRIBUTES --- - public $userDataDbTable = 'user_studiengang'; - public $userDataDbField = 'fach_id'; - - public $sortOrder = 3; - - // --- OPERATIONS --- - - /** - * @see UserFilterField::__construct - */ - public function __construct($fieldId = '') - { - parent::__construct($fieldId); - $this->validCompareOperators = [ - '!=' => ' ' - ]; - $this->validValues = ['' => ' ']; - } - - /** - * Get this field's display name. - * - * @return String - */ - public function getName() - { - return _('Alle Studienfächer'); - } -} 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + + * @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 @@ +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 @@ +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 @@ + 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 @@ + \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 @@ + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ + +