From d6ec870033743fcc07f8b6c71cb32450cc75abb6 Mon Sep 17 00:00:00 2001
From: Moritz Strohm <strohm@data-quest.de>
Date: Fri, 16 Dec 2022 13:44:16 +0000
Subject: [PATCH] StEP 1596, closes #1596

Closes #1596

Merge request studip/studip!1038
---
 app/controllers/accessibility/forms.php       | 175 ++++++++++++++++++
 .../accessibility/forms/report_barrier.php    |   2 +
 ...dd_accessibility_receiver_email_config.php |  33 ++++
 lib/classes/forms/Form.php                    |  69 ++++++-
 lib/classes/forms/Link.php                    | 166 +++++++++++++++++
 lib/classes/forms/Part.php                    |  36 ++++
 lib/classes/forms/Text.php                    |  72 +++++++
 lib/navigation/FooterNavigation.php           |   9 +
 locale/de/LC_MAILS/report_barrier.php         |  21 +++
 locale/en/LC_MAILS/report_barrier.php         |  21 +++
 templates/forms/form.php                      |   4 +-
 11 files changed, 599 insertions(+), 9 deletions(-)
 create mode 100644 app/controllers/accessibility/forms.php
 create mode 100644 app/views/accessibility/forms/report_barrier.php
 create mode 100644 db/migrations/5.3.15_add_accessibility_receiver_email_config.php
 create mode 100644 lib/classes/forms/Link.php
 create mode 100644 lib/classes/forms/Text.php
 create mode 100644 locale/de/LC_MAILS/report_barrier.php
 create mode 100644 locale/en/LC_MAILS/report_barrier.php

diff --git a/app/controllers/accessibility/forms.php b/app/controllers/accessibility/forms.php
new file mode 100644
index 00000000000..71ab2612f9f
--- /dev/null
+++ b/app/controllers/accessibility/forms.php
@@ -0,0 +1,175 @@
+<?php
+class Accessibility_FormsController extends StudipController
+{
+    protected $with_session = true;
+
+    public function report_barrier_action()
+    {
+        PageLayout::setTitle(_('Barriere melden'));
+
+        $this->page = Request::get('page');
+
+        $user = User::findCurrent();
+        $user_salutation = '';
+        if (!empty($user)) {
+            if ($user->geschlecht == 1) {
+                $user_salutation = _('Herr');
+            } elseif ($user->geschlecht == 2) {
+                $user_salutation = _('Frau');
+            } elseif ($user->geschlecht == 3) {
+                $user_salutation = _('divers');
+            }
+        }
+
+        $this->form = \Studip\Forms\Form::create();
+        $this->form->addInput(
+            new \Studip\Forms\HiddenInput(
+                'page',
+                '',
+                $this->page
+            )
+        );
+        $details_part = new \Studip\Forms\Fieldset(_('Angaben zur gefundenen Barriere'));
+        $details_part->addInput(
+            new \Studip\Forms\SelectInput(
+                'barrier_type',
+                _('Um welche Art von Barriere handelt es sich?'),
+                '',
+                [
+                    'options' => [
+                        _('Inhalte auf dieser Seite (z.B. PDF, Bilder oder Lernmodule)') => _('Inhalte auf dieser Seite (z.B. PDF, Bilder oder Lernmodule)'),
+                        _('Ein Problem mit der Seite selbst oder der Navigation') => _('Ein Problem mit der Seite selbst oder der Navigation'),
+                        _('Sonstiges') => _('Sonstiges')
+                    ]
+                ]
+            )
+        )->setRequired();
+        $details_part->addInput(
+            new \Studip\Forms\TextareaInput(
+                'barrier_details',
+                _('Beschreiben Sie die Barriere'),
+                ''
+            )
+        )->setRequired();
+        $this->form->addPart($details_part);
+        $personal_data_part = new \Studip\Forms\Fieldset(_('Ihre persönlichen Daten'));
+        $personal_data_part->addText(sprintf('<p>%s</p>', _('Geben Sie bitte Ihren Namen und Ihre E-Mail-Adresse an. Optional können Sie auch Ihre Telefonnummer angeben.')));
+        $personal_data_part->addInput(
+            new \Studip\Forms\SelectInput(
+                'salutation',
+                _('Anrede'),
+                $user_salutation,
+                [
+                    'options' => [
+                        _('Keine Angabe') => _('Keine Angabe'),
+                        _('Frau') => _('Frau'),
+                        _('Herr') => _('Herr'),
+                        _('divers') => _('divers')
+                    ]
+                ]
+            )
+        );
+        $personal_data_part->addInput(
+            new \Studip\Forms\TextInput(
+                'name',
+                _('Vorname und Nachname'),
+                $user ? sprintf('%s %s', $user->vorname, $user->nachname) : ''
+            )
+        )->setRequired();
+        $personal_data_part->addInput(
+            new \Studip\Forms\TextInput(
+                'phone_number',
+                _('Telefonnummer'),
+                $user ? ($user->privatcell ?: $user->privatnr) : ''
+            )
+        );
+        $personal_data_part->addInput(
+            new \Studip\Forms\TextInput(
+                'email_address',
+                _('E-Mail-Adresse'),
+                $user ? $user->email : ''
+            )
+        )->setRequired();
+        $privacy_url = Config::get()->PRIVACY_URL;
+
+        if (is_internal_url($privacy_url)) {
+            $personal_data_part->addLink(
+                _('Datenschutzerklärung lesen'),
+                URLHelper::getURL($privacy_url, ['cancel_login' => '1']),
+                Icon::create('link-intern'),
+                ['data-dialog' => 'size=big']
+            );
+        } else {
+            $personal_data_part->addLink(
+                _('Datenschutzerklärung lesen'),
+                URLHelper::getURL($privacy_url),
+                Icon::create('link-extern'),
+                ['target' => '_blank']
+            );
+        }
+        $personal_data_part->addInput(
+            new \Studip\Forms\CheckboxInput(
+                'confirm_privacy',
+                _('Ich habe die Datenschutzerklärung gelesen und akzeptiere sie.'),
+                ''
+            )
+        )->setRequired();
+        $this->form->addPart($personal_data_part);
+        $this->form->setSaveButtonText(_('Barriere melden'));
+        $this->form->setSaveButtonName('report');
+        $this->form->setURL($this->report_barrierURL());
+        $this->form->addStoreCallback(
+            function ($form, $form_values) {
+                $recipients = Config::get()->ACCESSIBILITY_RECEIVER_EMAIL;
+                if (empty($recipients)) {
+                    //Fallback: Use the UNI_CONTACT mail address:
+                    $recipients = [$GLOBALS['UNI_CONTACT']];
+                }
+                //Get the sender and their language:
+                $sender = User::findCurrent();
+                //Default to the system default language:
+                $lang = explode('_', $GLOBALS['DEFAULT_LANGUAGE'])[0];
+                if ($sender) {
+                    //Use the senders language since the choices in the form
+                    //are in their language as well.
+                    $lang = explode('_', getUserLanguage($sender->id))[0];
+                }
+                //Format the senders name according to the salutation.
+                $formatted_name = '';
+                if ($form_values['salutation'] === _('Keine Angabe')) {
+                    $formatted_name = $form_values['name'];
+                } elseif ($form_values['salutation'] === _('divers')) {
+                    $formatted_name = sprintf('%s (%s)', $form_values['name'], $form_values['salutation']);
+                } else {
+                    $formatted_name = sprintf('%s %s', $form_values['salutation'], $form_values['name']);
+                }
+                //Build the mail text:
+                $template = $GLOBALS['template_factory']->open("../locale/{$lang}/LC_MAILS/report_barrier.php");
+                $template->set_attributes([
+                    'sender' => $sender,
+                    'page' => $form_values['page'],
+                    'barrier_type' => $form_values['barrier_type'],
+                    'barrier_details' => $form_values['barrier_details'],
+                    'formatted_name' => $formatted_name,
+                    'phone_number' => $form_values['phone_number'],
+                    'email_address' => $form_values['email_address']
+                ]);
+                $mail_text = $template->render();
+
+                foreach ($recipients as $mail_address) {
+                    //Send the mail:
+                    $mail = new StudipMail();
+                    $mail->addRecipient($mail_address)
+                        ->setReplyToEmail($form_values['email_address'])
+                        ->setSubject(_('Meldung einer Barriere in Stud.IP'))
+                        ->setBodyText($mail_text)
+                        ->send();
+                }
+
+                $form->setSuccessMessage(_('Ihre Meldung einer Barriere wurde weitergeleitet.'));
+                return 1;
+            }
+        );
+        $this->form->autoStore();
+    }
+}
diff --git a/app/views/accessibility/forms/report_barrier.php b/app/views/accessibility/forms/report_barrier.php
new file mode 100644
index 00000000000..5dd8d7d9089
--- /dev/null
+++ b/app/views/accessibility/forms/report_barrier.php
@@ -0,0 +1,2 @@
+<?= MessageBox::info(_('Auf dieser Seite können Sie eine Barriere melden, die die Nutzbarkeit von Stud.IP für Sie einschränkt. Füllen Sie dazu das untenstehende Formular aus.'))->hideClose() ?></p>
+<?= $form->render() ?>
diff --git a/db/migrations/5.3.15_add_accessibility_receiver_email_config.php b/db/migrations/5.3.15_add_accessibility_receiver_email_config.php
new file mode 100644
index 00000000000..d3d73c242ae
--- /dev/null
+++ b/db/migrations/5.3.15_add_accessibility_receiver_email_config.php
@@ -0,0 +1,33 @@
+<?php
+
+class AddAccessibilityReceiverEmailConfig extends Migration
+{
+    public function description()
+    {
+        return 'Adds the configuration ACCESSIBILITY_RECEIVER_EMAIL, if it doesn\'t exist yet.';
+    }
+
+    protected function up()
+    {
+        $db = DBManager::get();
+
+        $db->exec(
+            "INSERT IGNORE INTO `config`
+             (`field`, `type`, `range`, `value`, `section`, `description`, `mkdate`, `chdate`)
+             VALUES
+             (
+                 'ACCESSIBILITY_RECEIVER_EMAIL', 'array', 'global', '', 'accessibility',
+                 'Die E-Mail-Adressen der Personen, die beim Melden einer Barriere benachrichtigt werden sollen.',
+                 UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
+             )"
+        );
+    }
+
+    protected function down()
+    {
+        $db = DBManager::get();
+
+        $db->exec("DELETE FROM `config_values` WHERE `field` = 'ACCESSIBILITY_RECEIVER_EMAIL'");
+        $db->exec("DELETE FROM `config` WHERE `field` = 'ACCESSIBILITY_RECEIVER_EMAIL'");
+    }
+}
diff --git a/lib/classes/forms/Form.php b/lib/classes/forms/Form.php
index b588ddb1a48..ba0258d2c4d 100644
--- a/lib/classes/forms/Form.php
+++ b/lib/classes/forms/Form.php
@@ -6,7 +6,7 @@ class Form extends Part
 {
 
     //models:
-    protected $afterStore = [];
+    protected $store_callbacks = [];
 
     //internals
     protected $inputs = [];
@@ -14,7 +14,12 @@ class Form extends Part
 
     //appearance in html-form
     protected $url = null;
+    protected $save_button_text = '';
+    protected $save_button_name = '';
+
     protected $autoStore = false;
+    protected $success_message = '';
+
     protected $collapsable = false;
 
     //to identify a form element
@@ -57,6 +62,8 @@ class Form extends Part
     final public function __construct(...$parts)
     {
         parent::__construct(...$parts);
+        //Set a default for the success message:
+        $this->success_message = _('Daten wurden gespeichert.');
     }
 
     /**
@@ -160,6 +167,48 @@ class Form extends Part
         return $this->url;
     }
 
+    /**
+     * Sets the text for the "save" button in the form.
+     *
+     * @param string $text The text for the button to save the form.
+     * @return $this
+     */
+    public function setSaveButtonText(string $text): Form
+    {
+        $this->save_button_text = $text;
+        return $this;
+    }
+
+    /**
+     * @return string The text for the "save" button in the form.
+     */
+    public function getSaveButtonText() : string
+    {
+        return $this->save_button_text ?: _('Speichern');
+    }
+
+    public function setSaveButtonName(string $name): Form
+    {
+        $this->save_button_name = $name;
+        return $this;
+    }
+
+    public function getSaveButtonName() : string
+    {
+        return $this->save_button_name ?: $this->getSaveButtonText();
+    }
+
+    public function setSuccessMessage(string $success_message): Form
+    {
+        $this->success_message = $success_message;
+        return $this;
+    }
+
+    public function getSuccessMessage() : string
+    {
+        return $this->success_message;
+    }
+
     public function setCollapsable($collapsing = true)
     {
         $this->collapsable = $collapsing;
@@ -182,7 +231,9 @@ class Form extends Part
         $this->autoStore = true;
         if (\Request::isPost() && \Request::isAjax() && !\Request::isDialog()) {
             $this->store();
-            \PageLayout::postSuccess(_('Daten wurden gespeichert.'));
+            if ($this->success_message) {
+                \PageLayout::postSuccess($this->success_message);
+            }
             page_close();
             die();
         }
@@ -200,9 +251,9 @@ class Form extends Part
      * @param callable $c
      * @return Form $this
      */
-    public function addAfterStoreCallback(Callable $c)
+    public function addStoreCallback(Callable $c): Form
     {
-        $this->afterStore[] = $c;
+        $this->store_callbacks[] = $c;
         return $this;
     }
 
@@ -240,11 +291,15 @@ class Form extends Part
         $stored = 0;
 
         //store by each input
+        $all_values = [];
         foreach ($this->getAllInputs() as $input) {
             $value = $this->getStorableValueFromRequest($input);
             if ($value !== null) {
                 $callback = $this->getStoringCallback($input);
-                $stored += $callback($value, $input);
+                if (is_callable($callback)) {
+                    $stored += $callback($value, $input);
+                }
+                $all_values[$input->getName()] = $value;
             }
         }
 
@@ -255,9 +310,9 @@ class Form extends Part
             }
         }
 
-        foreach ($this->afterStore as $callback) {
+        foreach ($this->store_callbacks as $callback) {
             if (is_callable($callback)) {
-                $stored += call_user_func($callback, $this);
+                $stored += call_user_func($callback, $this, $all_values);
             } else {
                 //throw warning if callback is not available:
                 if ($callback === null) {
diff --git a/lib/classes/forms/Link.php b/lib/classes/forms/Link.php
new file mode 100644
index 00000000000..6f26f28ef73
--- /dev/null
+++ b/lib/classes/forms/Link.php
@@ -0,0 +1,166 @@
+<?php
+namespace Studip\Forms;
+
+/**
+ * The Link class represents a part of a form that displays a link.
+ */
+class Link extends Part
+{
+    protected $url;
+    protected $label;
+    protected $icon;
+    protected $attributes = [];
+
+    public function __construct(string $url, string $label, \Icon $icon = null)
+    {
+        $this->url = $url;
+        $this->label = $label;
+        $this->icon = $icon;
+    }
+
+    /**
+     * Sets the url for the link.
+     *
+     * @param string $url
+     * @return $this
+     */
+    public function setURL(string $url): Link
+    {
+        $this->url = $url;
+        return $this;
+    }
+
+    /**
+     * Returns the url for the link.
+     * @return string
+     */
+    public function getURL(): string
+    {
+        return $this->url;
+    }
+
+    /**
+     * Sets the label for the link.
+     *
+     * @param string $label
+     * @return $this
+     */
+    public function setLabel(string $label): Link
+    {
+        $this->label = $label;
+        return $this;
+    }
+
+    /**
+     * Returns the label for the link.
+     *
+     * @return string
+     */
+    public function getLabel() : string
+    {
+        return $this->label;
+    }
+
+    /**
+     * Sets the icon for the link. May be null to remove the icon.
+     *
+     * @param \Icon $icon
+     * @return $this
+     */
+    public function setIcon(\Icon $icon = null): Link
+    {
+        $this->icon = $icon;
+        return $this;
+    }
+
+    /**
+     * Returns the icon for the link.
+     * @return \Icon|null
+     */
+    public function getIcon(): ?\Icon
+    {
+        return $this->icon;
+    }
+
+    /**
+     * Replaces all attributes for the link.
+     *
+     * @param array $attributes
+     * @return $this
+     */
+    public function setAttributes(array $attributes): Link
+    {
+        $this->attributes = $attributes;
+        return $this;
+    }
+
+    /**
+     * Adds/appends attributes to the current attributes for the link.
+     *
+     * @param array $attributes
+     * @return $this
+     */
+    public function addAttributes(array $attributes): Link
+    {
+        $this->attributes = array_merge($this->attributes, $attributes);
+        return $this;
+    }
+
+    /**
+     * Sets a single attribute for the link.
+     *
+     * @param string $key
+     * @param mixed $value
+     * @return $this
+     */
+    public function setAttribute(string $key, $value): Link
+    {
+        $this->attributes[$key] = $value;
+        return $this;
+    }
+
+    /**
+     * Returns the attributes for the link.
+     * @return array
+     */
+    public function getAttributes(): array
+    {
+        return $this->attributes;
+    }
+
+    /**
+     * Removes an attribute.
+     *
+     * @param string $key
+     * @param bool   $throw_exception Throw an exception if the attribute does not exists (default: false)
+     * @return $this
+     */
+    public function removeAttribute(string $key, bool $throw_exception = false): Link
+    {
+        if (!isset($this->attributes[$key]) && $throw_exception) {
+            throw new \RuntimeException("No attribute {$key} defined");
+        }
+
+        if (isset($this->attributes[$key])) {
+            unset($this->attributes[$key]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * "Renders" the text: Either return it directly, if it is HTML or call htmlReady first before returning it.
+     *
+     * @return string The text that shall be placed in the form, either as HTML or plain text.
+     */
+    public function render()
+    {
+        return sprintf(
+            '<div class="formpart"><a href="%1$s" %2$s>%3$s %4$s</a></div>',
+            \URLHelper::getLink($this->url, [], true),
+            arrayToHtmlAttributes($this->attributes),
+            $this->icon ? $this->icon->asImg(['class' => 'text-bottom']) : '',
+            htmlReady($this->label)
+        );
+    }
+}
diff --git a/lib/classes/forms/Part.php b/lib/classes/forms/Part.php
index 79c573bf761..4831945d13e 100644
--- a/lib/classes/forms/Part.php
+++ b/lib/classes/forms/Part.php
@@ -76,6 +76,42 @@ abstract class Part
         return $input;
     }
 
+    /**
+     * Adds a text block inside the form.
+     *
+     * @param string $text The text to be added.
+     * @param bool $text_is_html Whether the text is HTML (true) or plain text (false). Defaults to true.
+     * @return Text The added text form part.
+     */
+    public function addText(string $text, bool $text_is_html = true): Text
+    {
+        $text_part = new Text();
+        $text_part->setText($text, $text_is_html);
+        $text_part->setParent($this);
+        $this->parts[] = $text_part;
+        return $text_part;
+    }
+
+    /**
+     * Adds a link as a form part.
+     *
+     * @param string $title The title of the link.
+     * @param string $url The URL of the link.
+     * @param \Icon|null $icon The icon to be used for the link.
+     * @param array $attributes Additional link attributes.
+     *
+     * @return Link The Text form element containing the link as HTML.
+     */
+    public function addLink(string $title, string $url, ?\Icon $icon = null, array $attributes = []): Link
+    {
+        $link = new Link($url, $title, $icon);
+        $link->setAttributes($attributes);
+
+        $this->addPart($link);
+
+        return $link;
+    }
+
     /**
      * Renders this Part object. This could be a section or any other HTML element with child-elements.
      * @return string
diff --git a/lib/classes/forms/Text.php b/lib/classes/forms/Text.php
new file mode 100644
index 00000000000..611ff832ac7
--- /dev/null
+++ b/lib/classes/forms/Text.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Studip\Forms;
+
+/**
+ * The Text class represents a part of a form that just displays text.
+ * The text can either be HTML or unformatted text.
+ */
+class Text extends Part
+{
+    /**
+     * The text to be displayed.
+     */
+    protected $text = '';
+
+    /**
+     * This attribute defines whether to interpret the text as HTML (true) or as plain text (false).
+     */
+    protected $text_is_html = true;
+
+    /**
+     * Sets the text that shall be displayed in this form part.
+     *
+     * @param string $text The text to be displayed.
+     * @param bool $text_is_html Whether the text is HTML (true) or plain text. Defaults to true.
+     * @return $this This form part.
+     */
+    public function setText(string $text, bool $text_is_html = true): Text
+    {
+        $this->text = $text;
+        $this->text_is_html = $text_is_html;
+        return $this;
+    }
+
+    /**
+     * @return string The "raw form" of the text that shall be displayed.
+     */
+    public function getText() : string
+    {
+        return $this->text;
+    }
+
+    /**
+     * @return bool Whether the text is HTML (true) or not (false).
+     */
+    public function isHtmlText() : bool
+    {
+        return $this->text_is_html;
+    }
+
+    /**
+     * "Renders" the text: Either return it directly, if it is HTML or call htmlReady first before returning it.
+     *
+     * @return string The text that shall be placed in the form, either as HTML or plain text.
+     */
+    public function render()
+    {
+        if ($this->text_is_html) {
+            return $this->text;
+        } else {
+            return htmlReady($this->text);
+        }
+    }
+
+    /**
+     * @see Text::render()
+     */
+    public function renderWithCondition()
+    {
+        return $this->render();
+    }
+}
diff --git a/lib/navigation/FooterNavigation.php b/lib/navigation/FooterNavigation.php
index a24dc119b54..ac62ff8a4cb 100644
--- a/lib/navigation/FooterNavigation.php
+++ b/lib/navigation/FooterNavigation.php
@@ -47,5 +47,14 @@ class FooterNavigation extends Navigation
             $privacy_url = URLHelper::getURL($privacy_url, ['cancel_login' => '1']);
         }
         $this->addSubNavigation('privacy', new Navigation(_('Datenschutz'), $privacy_url));
+
+        $this->addSubNavigation(
+            'report_barrier',
+            new Navigation(
+                _('Barriere melden'),
+                URLHelper::getURL('dispatch.php/accessibility/forms/report_barrier', ['page' => Request::url()]),
+                ['data-dialog' => '']
+            )
+        );
     }
 }
diff --git a/locale/de/LC_MAILS/report_barrier.php b/locale/de/LC_MAILS/report_barrier.php
new file mode 100644
index 00000000000..7d7c28b0eeb
--- /dev/null
+++ b/locale/de/LC_MAILS/report_barrier.php
@@ -0,0 +1,21 @@
+Hallo,
+
+
+Die folgende Barriere, die Personen einschränkt bzw. behindert, wurde in Stud.IP entdeckt:
+
+<?= $barrier_type ?>
+
+
+<?= $barrier_details ?>
+
+
+Die Barriere befindet sich auf der Seite: <?= $page ?>
+
+
+Kontakt für Rückfragen:
+
+<?= $formatted_name ?>
+
+E-Mail: <?= $email_address ?>
+
+Telefon: <?= $phone_number ?>
diff --git a/locale/en/LC_MAILS/report_barrier.php b/locale/en/LC_MAILS/report_barrier.php
new file mode 100644
index 00000000000..6f5907b2ea4
--- /dev/null
+++ b/locale/en/LC_MAILS/report_barrier.php
@@ -0,0 +1,21 @@
+Hello
+
+
+The following barrier which restrict or impede persons has been discovered in Stud.IP:
+
+<?= $barrier_type ?>
+
+
+<?= $barrier_details ?>
+
+
+The barrier has been found on the page: <?= $page ?>
+
+
+Contact details for request:
+
+<?= $formatted_name ?>
+
+E-Mail: <?= $email_address ?>
+
+Phone: <?= $phone_number ?>
diff --git a/templates/forms/form.php b/templates/forms/form.php
index 2e1a661fbcc..21eb9c80f83 100644
--- a/templates/forms/form.php
+++ b/templates/forms/form.php
@@ -62,12 +62,12 @@ $form_id = md5(uniqid());
     </div>
     <? if (!Request::isDialog()) : ?>
         <footer>
-            <?= \Studip\Button::create(_('Speichern'), null, ['form' => $form_id]) ?>
+            <?= \Studip\Button::create($form->getSaveButtonText(), $form->getSaveButtonName(), ['form' => $form_id]) ?>
         </footer>
     <? endif ?>
 </form>
 <? if (Request::isDialog()) : ?>
     <footer data-dialog-button>
-        <?= \Studip\Button::create(_('Speichern'), null, ['form' => $form_id]) ?>
+        <?= \Studip\Button::create($form->getSaveButtonText(), $form->getSaveButtonName(), ['form' => $form_id]) ?>
     </footer>
 <? endif ?>
-- 
GitLab