]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Controllers/GaletteController.php
1d3b830effd596cc8d5ef85ecd4684a3d3946bce
[galette.git] / galette / lib / Galette / Controllers / GaletteController.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Galette main controller
7 *
8 * PHP version 5
9 *
10 * Copyright © 2019-2023 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category Entity
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2019-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.9.4dev - 2019-12-02
35 */
36
37 namespace Galette\Controllers;
38
39 use Galette\Entity\Social;
40 use Galette\Repository\PaymentTypes;
41 use Slim\Psr7\Request;
42 use Slim\Psr7\Response;
43 use Galette\Core\Logo;
44 use Galette\Core\PrintLogo;
45 use Galette\Core\Galette;
46 use Galette\Core\GaletteMail;
47 use Galette\Core\SysInfos;
48 use Galette\Entity\FieldsCategories;
49 use Galette\Entity\Status;
50 use Galette\Entity\Texts;
51 use Galette\Filters\MembersList;
52 use Galette\IO\News;
53 use Galette\IO\Charts;
54 use Galette\Repository\Members;
55 use Galette\Repository\Reminders;
56 use Analog\Analog;
57
58 /**
59 * Galette main controller
60 *
61 * @category Controllers
62 * @name GaletteController
63 * @package Galette
64 * @author Johan Cwiklinski <johan@x-tnd.be>
65 * @copyright 2019-2023 The Galette Team
66 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
67 * @link http://galette.tuxfamily.org
68 * @since Available since 0.9.4dev - 2019-12-02
69 */
70
71 class GaletteController extends AbstractController
72 {
73 /**
74 * Main route
75 *
76 * @param Request $request PSR Request
77 * @param Response $response PSR Response
78 *
79 * @return Response
80 */
81 public function slash(Request $request, Response $response): Response
82 {
83 return $this->galetteRedirect($request, $response);
84 }
85
86 /**
87 * System information
88 *
89 * @param Request $request PSR Request
90 * @param Response $response PSR Response
91 *
92 * @return Response
93 */
94 public function systemInformation(Request $request, Response $response): Response
95 {
96 $sysinfos = new SysInfos();
97 $raw_infos = $sysinfos->getRawData(
98 $this->zdb,
99 $this->preferences,
100 $this->plugins
101 );
102
103 // display page
104 $this->view->render(
105 $response,
106 'pages/sysinfos.html.twig',
107 array(
108 'page_title' => _T("System information"),
109 'rawinfos' => $raw_infos
110 )
111 );
112 return $response;
113 }
114
115 /**
116 * Dashboard page
117 *
118 * @param Request $request PSR Request
119 * @param Response $response PSR Response
120 *
121 * @return Response
122 */
123 public function dashboard(Request $request, Response $response): Response
124 {
125 $news = new News($this->preferences->pref_rss_url);
126
127 $params = [
128 'page_title' => _T("Dashboard"),
129 'contentcls' => 'desktop',
130 'news' => $news->getPosts(),
131 'show_dashboard' => $_COOKIE['show_galette_dashboard']
132 ];
133
134 $hide_telemetry = true;
135 if ($this->login->isAdmin()) {
136 $telemetry = new \Galette\Util\Telemetry(
137 $this->zdb,
138 $this->preferences,
139 $this->plugins
140 );
141 $params['reguuid'] = $telemetry->getRegistrationUuid();
142 $params['telemetry_sent'] = $telemetry->isSent();
143 $params['registered'] = $telemetry->isRegistered();
144
145 $hide_telemetry = $telemetry->isSent() && $telemetry->isRegistered()
146 || isset($_COOKIE['hide_galette_telemetry']) && $_COOKIE['hide_galette_telemetry'];
147 }
148 $params['hide_telemetry'] = $hide_telemetry;
149
150 // display page
151 $this->view->render(
152 $response,
153 'pages/desktop.html.twig',
154 $params
155 );
156 return $response;
157 }
158
159 /**
160 * Preferences page
161 *
162 * @param Request $request PSR Request
163 * @param Response $response PSR Response
164 *
165 * @return Response
166 */
167 public function preferences(Request $request, Response $response): Response
168 {
169 // flagging required fields
170 $required = array(
171 'pref_nom' => 1,
172 'pref_lang' => 1,
173 'pref_numrows' => 1,
174 'pref_log' => 1,
175 'pref_statut' => 1,
176 'pref_etiq_marges_v' => 1,
177 'pref_etiq_marges_h' => 1,
178 'pref_etiq_hspace' => 1,
179 'pref_etiq_vspace' => 1,
180 'pref_etiq_hsize' => 1,
181 'pref_etiq_vsize' => 1,
182 'pref_etiq_cols' => 1,
183 'pref_etiq_rows' => 1,
184 'pref_etiq_corps' => 1,
185 'pref_card_abrev' => 1,
186 'pref_card_strip' => 1,
187 'pref_card_marges_v' => 1,
188 'pref_card_marges_h' => 1,
189 'pref_card_hspace' => 1,
190 'pref_card_vspace' => 1
191 );
192
193 if ($this->login->isSuperAdmin() && GALETTE_MODE !== Galette::MODE_DEMO) {
194 $required['pref_admin_login'] = 1;
195 }
196
197 $prefs_fields = $this->preferences->getFieldsNames();
198 // collect data
199 $pref = [];
200 foreach ($prefs_fields as $fieldname) {
201 $pref[$fieldname] = $this->preferences->$fieldname;
202 }
203
204 //on error, user values are stored into session
205 if ($this->session->entered_preferences) {
206 $pref = array_merge($pref, $this->session->entered_preferences);
207 $this->session->entered_preferences = null;
208 }
209
210 //List available themes
211 $themes = array();
212 $d = dir(GALETTE_THEMES_PATH);
213 while (($entry = $d->read()) !== false) {
214 $full_entry = GALETTE_THEMES_PATH . $entry;
215 if (
216 $entry != '.'
217 && $entry != '..'
218 && is_dir($full_entry)
219 && file_exists($full_entry . '/page.html.twig')
220 ) {
221 $themes[] = $entry;
222 }
223 }
224 $d->close();
225
226 //List payment types for default to be selected
227 $ptypes = new PaymentTypes(
228 $this->zdb,
229 $this->preferences,
230 $this->login
231 );
232 $ptlist = $ptypes->getList();
233
234 $m = new Members();
235 $s = new Status($this->zdb);
236
237 //Active tab on page
238 $tab = $request->getQueryParams()['tab'] ?? 'general';
239
240 // display page
241 $this->view->render(
242 $response,
243 'pages/preferences.html.twig',
244 array(
245 'page_title' => _T("Settings"),
246 'staff_members' => $m->getStaffMembersList(true),
247 'time' => time(),
248 'pref' => $pref,
249 'pref_numrows_options' => array(
250 10 => '10',
251 20 => '20',
252 50 => '50',
253 100 => '100'
254 ),
255 'print_logo' => $this->print_logo,
256 'required' => $required,
257 'themes' => $themes,
258 'statuts' => $s->getList(),
259 'accounts_options' => array(
260 Members::ALL_ACCOUNTS => _T("All accounts"),
261 Members::ACTIVE_ACCOUNT => _T("Active accounts"),
262 Members::INACTIVE_ACCOUNT => _T("Inactive accounts")
263 ),
264 'paymenttypes' => $ptlist,
265 'osocials' => new Social($this->zdb),
266 'tab' => $tab
267 )
268 );
269 return $response;
270 }
271
272 /**
273 * Store preferences
274 *
275 * @param Request $request PSR Request
276 * @param Response $response PSR Response
277 *
278 * @return Response
279 */
280 public function storePreferences(Request $request, Response $response): Response
281 {
282 $post = $request->getParsedBody();
283 $error_detected = [];
284 $warning_detected = [];
285
286 // Validation
287 if (isset($post['valid']) && $post['valid'] == '1') {
288 if ($this->preferences->check($post, $this->login)) {
289 if (!$this->preferences->store()) {
290 $error_detected[] = _T("An SQL error has occurred while storing preferences. Please try again, and contact the administrator if the problem persists.");
291 } else {
292 $this->flash->addMessage(
293 'success_detected',
294 _T("Preferences has been saved.")
295 );
296 }
297 $warning_detected = array_merge($warning_detected, $this->preferences->checkCardsSizes());
298
299 // picture upload
300 if (GALETTE_MODE !== Galette::MODE_DEMO && isset($_FILES['logo'])) {
301 if ($_FILES['logo']['error'] === UPLOAD_ERR_OK) {
302 if ($_FILES['logo']['tmp_name'] != '') {
303 if (is_uploaded_file($_FILES['logo']['tmp_name'])) {
304 $res = $this->logo->store($_FILES['logo']);
305 if ($res < 0) {
306 $error_detected[] = $this->logo->getErrorMessage($res);
307 } else {
308 $this->logo = new Logo();
309 }
310 }
311 }
312 } elseif ($_FILES['logo']['error'] !== UPLOAD_ERR_NO_FILE) {
313 Analog::log(
314 $this->logo->getPhpErrorMessage($_FILES['logo']['error']),
315 Analog::WARNING
316 );
317 $error_detected[] = $this->logo->getPhpErrorMessage(
318 $_FILES['logo']['error']
319 );
320 }
321 }
322
323 if (GALETTE_MODE !== Galette::MODE_DEMO && isset($post['del_logo'])) {
324 if (!$this->logo->delete()) {
325 $error_detected[] = _T("Delete failed");
326 } else {
327 $this->logo = new Logo(); //get default Logo
328 }
329 }
330
331 // Card logo upload
332 if (GALETTE_MODE !== Galette::MODE_DEMO && isset($_FILES['card_logo'])) {
333 if ($_FILES['card_logo']['error'] === UPLOAD_ERR_OK) {
334 if ($_FILES['card_logo']['tmp_name'] != '') {
335 if (is_uploaded_file($_FILES['card_logo']['tmp_name'])) {
336 $res = $this->print_logo->store($_FILES['card_logo']);
337 if ($res < 0) {
338 $error_detected[] = $this->print_logo->getErrorMessage($res);
339 } else {
340 $this->print_logo = new PrintLogo();
341 }
342 }
343 }
344 } elseif ($_FILES['card_logo']['error'] !== UPLOAD_ERR_NO_FILE) {
345 Analog::log(
346 $this->print_logo->getPhpErrorMessage($_FILES['card_logo']['error']),
347 Analog::WARNING
348 );
349 $error_detected[] = $this->print_logo->getPhpErrorMessage(
350 $_FILES['card_logo']['error']
351 );
352 }
353 }
354
355 if (GALETTE_MODE !== Galette::MODE_DEMO && isset($post['del_card_logo'])) {
356 if (!$this->print_logo->delete()) {
357 $error_detected[] = _T("Delete failed");
358 } else {
359 $this->print_logo = new PrintLogo();
360 }
361 }
362 } else {
363 $error_detected = $this->preferences->getErrors();
364 }
365
366 if (count($error_detected) > 0) {
367 $this->session->entered_preferences = $post;
368 //report errors
369 foreach ($error_detected as $error) {
370 $this->flash->addMessage(
371 'error_detected',
372 $error
373 );
374 }
375 }
376
377 if (count($warning_detected) > 0) {
378 //report warnings
379 foreach ($warning_detected as $warning) {
380 $this->flash->addMessage(
381 'warning_detected',
382 $warning
383 );
384 }
385 }
386 }
387 if (isset($post['tab']) && $post['tab'] != 'general') {
388 $tab = '?tab=' . $post['tab'];
389 } else {
390 $tab = '';
391 }
392 return $response
393 ->withStatus(301)
394 ->withHeader('Location', $this->routeparser->urlFor('preferences') . $tab);
395 }
396
397 /**
398 * Test mail parameters
399 *
400 * @param Request $request PSR Request
401 * @param Response $response PSR Response
402 *
403 * @return Response
404 */
405 public function testEmail(Request $request, Response $response): Response
406 {
407 $sent = false;
408 if (!$this->preferences->pref_mail_method > GaletteMail::METHOD_DISABLED) {
409 $this->flash->addMessage(
410 'error_detected',
411 _T("You asked Galette to send a test email, but email has been disabled in the preferences.")
412 );
413 } else {
414 $get = $request->getQueryParams();
415 $dest = (isset($get['adress']) ? $get['adress'] : $this->preferences->pref_email_newadh);
416 if (GaletteMail::isValidEmail($dest)) {
417 $mail = new GaletteMail($this->preferences);
418 $mail->setSubject(_T('Test message'));
419 $mail->setRecipients(
420 array(
421 $dest => _T("Galette admin")
422 )
423 );
424 $mail->setMessage(_T('Test message.'));
425 $sent = $mail->send();
426
427 if ($sent) {
428 $this->flash->addMessage(
429 'success_detected',
430 str_replace(
431 '%email',
432 $dest,
433 _T("An email has been sent to %email")
434 )
435 );
436 } else {
437 $this->flash->addMessage(
438 'error_detected',
439 str_replace(
440 '%email',
441 $dest,
442 _T("No email sent to %email")
443 )
444 );
445 }
446 } else {
447 $this->flash->addMessage(
448 'error_detected',
449 _T("Invalid email adress!")
450 );
451 }
452 }
453
454 if (!($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')) {
455 return $response
456 ->withStatus(301)
457 ->withHeader('Location', $this->routeparser->urlFor('preferences'));
458 } else {
459 return $this->withJson(
460 $response,
461 [
462 'sent' => $sent
463 ]
464 );
465 }
466 }
467
468 /**
469 * Charts page
470 *
471 * @param Request $request PSR Request
472 * @param Response $response PSR Response
473 *
474 * @return Response
475 */
476 public function charts(Request $request, Response $response): Response
477 {
478 $charts = new Charts(
479 array(
480 Charts::MEMBERS_STATUS_PIE,
481 Charts::MEMBERS_STATEDUE_PIE,
482 Charts::CONTRIBS_TYPES_PIE,
483 Charts::COMPANIES_OR_NOT,
484 Charts::CONTRIBS_ALLTIME
485 )
486 );
487
488 // display page
489 $this->view->render(
490 $response,
491 'pages/charts.html.twig',
492 array(
493 'page_title' => _T("Charts"),
494 'charts' => $charts->getCharts(),
495 'require_charts' => true
496 )
497 );
498 return $response;
499 }
500
501 /**
502 * Core fields configuration page
503 *
504 * @param Request $request PSR Request
505 * @param Response $response PSR Response
506 *
507 * @return Response
508 */
509 public function configureCoreFields(Request $request, Response $response): Response
510 {
511 $fc = $this->fields_config;
512
513 $params = [
514 'page_title' => _T("Fields configuration"),
515 'time' => time(),
516 'categories' => FieldsCategories::getList($this->zdb),
517 'categorized_fields' => $fc->getCategorizedFields(),
518 'non_required' => $fc->getNonRequired()
519 ];
520
521 // display page
522 $this->view->render(
523 $response,
524 'pages/configuration_core_fields.html.twig',
525 $params
526 );
527 return $response;
528 }
529
530 /**
531 * Process core fields configuration
532 *
533 * @param Request $request PSR Request
534 * @param Response $response PSR Response
535 *
536 * @return Response
537 */
538 public function storeCoreFieldsConfig(Request $request, Response $response): Response
539 {
540 $post = $request->getParsedBody();
541 $fc = $this->fields_config;
542
543 $pos = 0;
544 $current_cat = 0;
545 $res = array();
546 foreach ($post['fields'] as $abs_pos => $field) {
547 if ($current_cat != $post[$field . '_category']) {
548 //reset position when category has changed
549 $pos = 0;
550 //set new current category
551 $current_cat = $post[$field . '_category'];
552 }
553
554 $required = null;
555 if (isset($post[$field . '_required'])) {
556 $required = $post[$field . '_required'];
557 } else {
558 $required = false;
559 }
560
561 $res[$current_cat][] = array(
562 'field_id' => $field,
563 'label' => htmlspecialchars($post[$field . '_label'], ENT_QUOTES),
564 'category' => $post[$field . '_category'],
565 'visible' => $post[$field . '_visible'],
566 'required' => $required
567 );
568 $pos++;
569 }
570 //okay, we've got the new array, we send it to the
571 //Object that will store it in the database
572 $success = $fc->setFields($res);
573 FieldsCategories::setCategories($this->zdb, $post['categories']);
574 if ($success === true) {
575 $this->flash->addMessage(
576 'success_detected',
577 _T("Fields configuration has been successfully stored")
578 );
579 } else {
580 $this->flash->addMessage(
581 'error_detected',
582 _T("An error occurred while storing fields configuration :(")
583 );
584 }
585
586 return $response
587 ->withStatus(301)
588 ->withHeader('Location', $this->routeparser->urlFor('configureCoreFields'));
589 }
590
591 /**
592 * Core lists configuration page
593 *
594 * @param Request $request PSR Request
595 * @param Response $response PSR Response
596 * @param string $table Tbale name
597 *
598 * @return Response
599 */
600 public function configureListFields(Request $request, Response $response, string $table): Response
601 {
602 //TODO: check if type table exists
603
604 $lc = $this->lists_config;
605
606 $params = [
607 'page_title' => _T("Lists configuration"),
608 'table' => $table,
609 'time' => time(),
610 'listed_fields' => $lc->getListedFields(),
611 'remaining_fields' => $lc->getRemainingFields()
612 ];
613
614 // display page
615 $this->view->render(
616 $response,
617 'pages/configuration_core_lists.html.twig',
618 $params
619 );
620 return $response;
621 }
622
623 /**
624 * Process list fields configuration
625 *
626 * @param Request $request PSR Request
627 * @param Response $response PSR Response
628 *
629 * @return Response
630 */
631 public function storeListFields(Request $request, Response $response): Response
632 {
633 $post = $request->getParsedBody();
634
635 $lc = $this->lists_config;
636 $fields = [];
637 foreach ($post['fields'] as $field) {
638 $fields[] = $lc->getField($field);
639 }
640 $success = $lc->setListFields($fields);
641
642 if ($success === true) {
643 $this->flash->addMessage(
644 'success_detected',
645 _T("List configuration has been successfully stored")
646 );
647 } else {
648 $this->flash->addMessage(
649 'error_detected',
650 _T("An error occurred while storing list configuration :(")
651 );
652 }
653
654 return $response
655 ->withStatus(301)
656 ->withHeader('Location', $this->routeparser->urlFor('configureListFields', $this->getArgs($request)));
657 }
658
659 /**
660 * Reminders page
661 *
662 * @param Request $request PSR Request
663 * @param Response $response PSR Response
664 *
665 * @return Response
666 */
667 public function reminders(Request $request, Response $response): Response
668 {
669 $texts = new Texts($this->preferences, $this->routeparser);
670
671 $previews = array(
672 'impending' => $texts->getTexts('impendingduedate', $this->preferences->pref_lang),
673 'late' => $texts->getTexts('lateduedate', $this->preferences->pref_lang)
674 );
675
676 $members = new Members();
677 $reminders = $members->getRemindersCount();
678
679 // display page
680 $this->view->render(
681 $response,
682 'pages/reminder.html.twig',
683 [
684 'page_title' => _T("Reminders"),
685 'previews' => $previews,
686 'count_impending' => $reminders['impending'],
687 'count_impending_nomail' => $reminders['nomail']['impending'],
688 'count_late' => $reminders['late'],
689 'count_late_nomail' => $reminders['nomail']['late']
690 ]
691 );
692 return $response;
693 }
694
695 /**
696 * Send reminders
697 *
698 * @param Request $request PSR Request
699 * @param Response $response PSR Response
700 *
701 * @return Response
702 */
703 public function doReminders(Request $request, Response $response): Response
704 {
705 $error_detected = [];
706 $warning_detected = [];
707 $success_detected = [];
708
709 $post = $request->getParsedBody();
710 $texts = new Texts($this->preferences, $this->routeparser);
711 $selected = null;
712 if (isset($post['reminders'])) {
713 $selected = $post['reminders'];
714 }
715 $reminders = new Reminders($selected);
716
717 $labels = false;
718 $labels_members = array();
719 if (isset($post['reminder_wo_mail'])) {
720 $labels = true;
721 }
722
723 $list_reminders = $reminders->getList($this->zdb, $labels);
724 if (count($list_reminders) == 0) {
725 $warning_detected[] = _T("No reminder to send for now.");
726 } else {
727 foreach ($list_reminders as $reminder) {
728 if ($labels === false) {
729 $reminder
730 ->setDb($this->zdb)
731 ->setLogin($this->login)
732 ->setPreferences($this->preferences)
733 ->setRouteParser($this->routeparser)
734 ;
735 //send reminders by email
736 $sent = $reminder->send($texts, $this->history, $this->zdb);
737
738 if ($sent === true) {
739 $success_detected[] = $reminder->getMessage();
740 } else {
741 $error_detected[] = $reminder->getMessage();
742 }
743 } else {
744 //generate labels for members without email address
745 $labels_members[] = $reminder->member_id;
746 }
747 }
748
749 if ($labels === true) {
750 if (count($labels_members) > 0) {
751 $session_var = 'filters_reminders_labels';
752 $labels_filters = new MembersList();
753 $labels_filters->selected = $labels_members;
754 $this->session->$session_var = $labels_filters;
755 return $response
756 ->withStatus(307)
757 ->withHeader(
758 'Location',
759 $this->routeparser->urlFor('pdf-members-labels') . '?session_var=' . $session_var
760 );
761 } else {
762 $error_detected[] = _T("There are no member to proceed.");
763 }
764 }
765
766 if (count($error_detected) > 0) {
767 array_unshift(
768 $error_detected,
769 _T("Reminder has not been sent:")
770 );
771 }
772
773 if (count($success_detected) > 0) {
774 array_unshift(
775 $success_detected,
776 _T("Sent reminders:")
777 );
778 }
779 }
780
781 //flash messages if any
782 if (count($error_detected) > 0) {
783 foreach ($error_detected as $error) {
784 $this->flash->addMessage('error_detected', $error);
785 }
786 }
787 if (count($warning_detected) > 0) {
788 foreach ($warning_detected as $warning) {
789 $this->flash->addMessage('warning_detected', $warning);
790 }
791 }
792 if (count($success_detected) > 0) {
793 foreach ($success_detected as $success) {
794 $this->flash->addMessage('success_detected', $success);
795 }
796 }
797
798 return $response
799 ->withStatus(301)
800 ->withHeader('Location', $this->routeparser->urlFor('reminders'));
801 }
802
803 /**
804 * Main route
805 *
806 * @param Request $request PSR Request
807 * @param Response $response PSR Response
808 * @param string $membership Either 'late' or 'nearly'
809 * @param string $mail Either 'withmail' or 'withoutmail'
810 *
811 * @return Response
812 */
813 public function filterReminders(Request $request, Response $response, string $membership, string $mail): Response
814 {
815 //always reset filters
816 $filters = new MembersList();
817 $filters->filter_account = Members::ACTIVE_ACCOUNT;
818
819 $membership = ($membership === 'nearly' ?
820 Members::MEMBERSHIP_NEARLY : Members::MEMBERSHIP_LATE);
821 $filters->membership_filter = $membership;
822
823 //TODO: filter on reminder may take care of parent email as well
824 $mail = ($mail === 'withmail' ?
825 Members::FILTER_W_EMAIL : Members::FILTER_WO_EMAIL);
826 $filters->email_filter = $mail;
827
828 $this->session->filter_members = $filters;
829
830 return $response
831 ->withStatus(301)
832 ->withHeader('Location', $this->routeparser->urlFor('members'));
833 }
834
835 /**
836 * Direct document page
837 *
838 * @param Request $request PSR Request
839 * @param Response $response PSR Response
840 * @param string $hash Hash
841 *
842 * @return Response
843 */
844 public function documentLink(Request $request, Response $response, string $hash): Response
845 {
846 // display page
847 $this->view->render(
848 $response,
849 'pages/directlink.html.twig',
850 array(
851 'hash' => $hash,
852 'page_title' => _T('Download document')
853 )
854 );
855 return $response;
856 }
857
858 /**
859 * Main route
860 *
861 * @param Request $request PSR Request
862 * @param Response $response PSR Response
863 *
864 * @return Response
865 */
866 public function favicon(Request $request, Response $response): Response
867 {
868 return $response;
869 }
870 }