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