]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Core/Preferences.php
Add option to disable social networks on members
[galette.git] / galette / lib / Galette / Core / Preferences.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\Core;
23
24 use Galette\Entity\PaymentType;
25 use Galette\Entity\Social;
26 use Galette\Features\Replacements;
27 use Galette\Features\Socials;
28 use PHPMailer\PHPMailer\PHPMailer;
29 use Throwable;
30 use Analog\Analog;
31 use Galette\Entity\Adherent;
32 use Galette\Entity\Status;
33 use Galette\IO\PdfMembersCards;
34 use Galette\Repository\Members;
35
36 /**
37 * Preferences for galette
38 *
39 * @author Johan Cwiklinski <johan@x-tnd.be>
40 *
41 * @property string $pref_admin_login Super admin login
42 * @property string $pref_admin_pass Super admin password
43 * @property string $pref_nom Association name
44 * @property string $pref_slogan Association slogan
45 * @property string $pref_adresse Address
46 * @property string $pref_adresse2 Address continuation
47 * @property string $pref_cp Association zipcode
48 * @property string $pref_ville Association
49 * @property string $pref_region Region
50 * @property string $pref_pays Country
51 * @property integer $pref_postal_adress Postal adress to use, one of self::POSTAL_ADDRESS*
52 * @property integer $pref_postal_staff_member Staff member ID from which retrieve postal address
53 * @property boolean $pref_disable_members_socials
54 * @property string $pref_lang Default instance language
55 * @property integer $pref_numrows Default number of rows in lists
56 * @property integer $pref_log History, one of self::LOG_*
57 * @property integer $pref_statut Default status for new members
58 * @property string $pref_email_nom
59 * @property string $pref_email
60 * @property string $pref_email_newadh
61 * @property boolean $pref_bool_mailadh
62 * @property boolean $pref_bool_mailowner
63 * @property boolean $pref_editor_enabled
64 * @property integer $pref_mail_method Mail method, see GaletteMail::METHOD_*
65 * @property string $pref_mail_smtp
66 * @property string $pref_mail_smtp_host
67 * @property boolean $pref_mail_smtp_auth
68 * @property boolean $pref_mail_smtp_secure
69 * @property integer $pref_mail_smtp_port
70 * @property string $pref_mail_smtp_user
71 * @property string $pref_mail_smtp_password
72 * @property integer $pref_membership_ext
73 * @property string $pref_beg_membership
74 * @property integer $pref_membership_offermonths
75 * @property string $pref_email_reply_to
76 * @property string $pref_website
77 * @property integer $pref_etiq_marges_v
78 * @property integer $pref_etiq_marges_h
79 * @property integer $pref_etiq_hspace
80 * @property integer $pref_etiq_vspace
81 * @property integer $pref_etiq_hsize
82 * @property integer $pref_etiq_vsize
83 * @property integer $pref_etiq_cols
84 * @property integer $pref_etiq_rows
85 * @property integer $pref_etiq_corps
86 * @property boolean $pref_etiq_border
87 * @property boolean $pref_force_picture_ratio
88 * @property string $pref_member_picture_ratio
89 * @property string $pref_card_abrev
90 * @property string $pref_card_strip
91 * @property string $pref_card_tcol
92 * @property string $pref_card_scol
93 * @property string $pref_card_bcol
94 * @property string $pref_card_hcol
95 * @property string $pref_bool_display_title
96 * @property integer $pref_card_address
97 * @property string $pref_card_year
98 * @property integer $pref_card_marges_v
99 * @property integer $pref_card_marges_h
100 * @property integer $pref_card_vspace
101 * @property integer $pref_card_hspace
102 * @property string $pref_card_self
103 * @property string $pref_theme Prefered theme
104 * @property boolean $pref_bool_publicpages
105 * @property integer $pref_publicpages_visibility
106 * @property boolean $pref_bool_selfsubscribe
107 * @property string $pref_member_form_grid
108 * @property string $pref_mail_sign
109 * @property string $pref_new_contrib_script
110 * @property boolean $pref_bool_wrap_mails
111 * @property string $pref_rss_url
112 * @property boolean $pref_show_id
113 * @property string $pref_adhesion_form
114 * @property boolean $pref_mail_allow_unsecure
115 * @property string $pref_instance_uuid
116 * @property string $pref_registration_uuid
117 * @property string $pref_telemetry_date
118 * @property string $pref_registration_date
119 * @property string $pref_footer
120 * @property integer $pref_filter_account
121 * @property string $pref_galette_url
122 * @property integer $pref_redirect_on_create
123 * @property integer $pref_password_length
124 * @property boolean $pref_password_blacklist
125 * @property integer $pref_password_strength
126 * @property integer $pref_default_paymenttype
127 * @property boolean $pref_bool_create_member
128 * @property boolean $pref_bool_groupsmanagers_create_member
129 * @property boolean $pref_bool_groupsmanagers_edit_member
130 * @property boolean $pref_bool_groupsmanagers_edit_groups
131 * @property boolean $pref_bool_groupsmanagers_mailings
132 * @property boolean $pref_bool_groupsmanagers_exports
133 * @property-read array $vpref_email_newadh list of mail senders
134 * @property-read array $vpref_email list of mail senders
135 * @property boolean $pref_noindex
136 */
137 class Preferences
138 {
139 use Replacements;
140 use Socials;
141
142 protected Preferences $preferences; //redefined from Replacements feature - avoid circular dependency
143 /** @var array<string, bool|int|string> */
144 private array $prefs;
145 /** @var array<string> */
146 private array $errors = [];
147
148 public const TABLE = 'preferences';
149 public const PK = 'nom_pref';
150
151 /** Postal address will be the one given in the preferences */
152 public const POSTAL_ADDRESS_FROM_PREFS = 0;
153 /** Postal address will be the one of the selected staff member */
154 public const POSTAL_ADDRESS_FROM_STAFF = 1;
155
156 /** Public pages stuff */
157 /** Public pages are publically visibles */
158 public const PUBLIC_PAGES_VISIBILITY_PUBLIC = 0;
159 /** Public pages are visibles for up to date members only */
160 public const PUBLIC_PAGES_VISIBILITY_RESTRICTED = 1;
161 /** Public pages are visibles for admin and staff members only */
162 public const PUBLIC_PAGES_VISIBILITY_PRIVATE = 2;
163
164 public const LOG_DISABLED = 0;
165 public const LOG_ENABLED = 1;
166
167 /** No password strength */
168 public const PWD_NONE = 0;
169 /** Weak password strength */
170 public const PWD_WEAK = 1;
171 /** Medium password strength */
172 public const PWD_MEDIUM = 2;
173 /** Strong password strength */
174 public const PWD_STRONG = 3;
175 /** Very strong password strength */
176 public const PWD_VERY_STRONG = 4;
177
178 /** @var array<string> */
179 private static array $fields = array(
180 'nom_pref',
181 'val_pref'
182 );
183
184 /** @var array<string, bool|int|string> */
185 private static array $defaults = array(
186 'pref_admin_login' => 'admin',
187 'pref_admin_pass' => 'admin',
188 'pref_nom' => 'Galette',
189 'pref_slogan' => '',
190 'pref_adresse' => '-',
191 'pref_adresse2' => '',
192 'pref_cp' => '',
193 'pref_ville' => '',
194 'pref_region' => '',
195 'pref_pays' => '',
196 'pref_postal_adress' => self::POSTAL_ADDRESS_FROM_PREFS,
197 'pref_postal_staff_member' => '',
198 'pref_disable_members_socials' => false,
199 'pref_lang' => I18n::DEFAULT_LANG,
200 'pref_numrows' => 30,
201 'pref_log' => self::LOG_ENABLED,
202 'pref_statut' => Status::DEFAULT_STATUS,
203 /* Preferences for emails */
204 'pref_email_nom' => 'Galette',
205 'pref_email' => 'mail@domain.com',
206 'pref_email_newadh' => 'mail@domain.com',
207 'pref_bool_mailadh' => false,
208 'pref_bool_mailowner' => false,
209 'pref_editor_enabled' => false,
210 'pref_mail_method' => GaletteMail::METHOD_DISABLED,
211 'pref_mail_smtp' => '',
212 'pref_mail_smtp_host' => '',
213 'pref_mail_smtp_auth' => false,
214 'pref_mail_smtp_secure' => false,
215 'pref_mail_smtp_port' => '',
216 'pref_mail_smtp_user' => '',
217 'pref_mail_smtp_password' => '',
218 'pref_membership_ext' => 12,
219 'pref_beg_membership' => '',
220 'pref_membership_offermonths' => 0,
221 'pref_email_reply_to' => '',
222 'pref_website' => '',
223 /* Preferences for labels */
224 'pref_etiq_marges_v' => 10,
225 'pref_etiq_marges_h' => 10,
226 'pref_etiq_hspace' => 10,
227 'pref_etiq_vspace' => 5,
228 'pref_etiq_hsize' => 90,
229 'pref_etiq_vsize' => 35,
230 'pref_etiq_cols' => 2,
231 'pref_etiq_rows' => 7,
232 'pref_etiq_corps' => 12,
233 'pref_etiq_border' => true,
234 /* Preferences for members cards */
235 'pref_force_picture_ratio' => false,
236 'pref_member_picture_ratio' => 'square_ratio',
237 'pref_card_abrev' => 'GALETTE',
238 'pref_card_strip' => 'Gestion d\'Adherents en Ligne Extrêmement Tarabiscotée',
239 'pref_card_tcol' => '#FFFFFF',
240 'pref_card_scol' => '#8C2453',
241 'pref_card_bcol' => '#53248C',
242 'pref_card_hcol' => '#248C53',
243 'pref_bool_display_title' => false,
244 'pref_card_address' => 1,
245 'pref_card_year' => '',
246 'pref_card_marges_v' => 15,
247 'pref_card_marges_h' => 20,
248 'pref_card_vspace' => 5,
249 'pref_card_hspace' => 10,
250 'pref_card_self' => 1,
251 'pref_theme' => 'default',
252 'pref_bool_publicpages' => true,
253 'pref_publicpages_visibility' => self::PUBLIC_PAGES_VISIBILITY_RESTRICTED,
254 'pref_mail_sign' => "{ASSO_NAME}\r\n\r\n{ASSO_WEBSITE}",
255 /* Preferences for member/subscribe form */
256 'pref_bool_selfsubscribe' => true,
257 'pref_member_form_grid' => 'one',
258 /* New contribution script */
259 'pref_new_contrib_script' => '',
260 'pref_bool_wrap_mails' => true,
261 'pref_rss_url' => 'https://galette.eu/dc/index.php/feed/atom',
262 'pref_show_id' => false,
263 'pref_adhesion_form' => '\Galette\IO\PdfAdhesionForm',
264 'pref_mail_allow_unsecure' => false,
265 'pref_instance_uuid' => '',
266 'pref_registration_uuid' => '',
267 'pref_telemetry_date' => '',
268 'pref_registration_date' => '',
269 'pref_footer' => '',
270 'pref_filter_account' => Members::ALL_ACCOUNTS,
271 'pref_galette_url' => '',
272 'pref_redirect_on_create' => Adherent::AFTER_ADD_DEFAULT,
273 /* Security related */
274 'pref_password_length' => 6,
275 'pref_password_blacklist' => false,
276 'pref_password_strength' => self::PWD_NONE,
277 'pref_default_paymenttype' => PaymentType::CHECK,
278 'pref_bool_create_member' => false,
279 'pref_bool_groupsmanagers_create_member' => false,
280 'pref_bool_groupsmanagers_edit_member' => false,
281 'pref_bool_groupsmanagers_edit_groups' => false,
282 'pref_bool_groupsmanagers_mailings' => false,
283 'pref_bool_groupsmanagers_exports' => true,
284 'pref_noindex' => false
285 );
286
287 /** @var Social[] */
288 private array $socials;
289
290 // flagging required fields
291 /** @var array<string> */
292 private array $required = array(
293 'pref_nom',
294 'pref_lang',
295 'pref_numrows',
296 'pref_log',
297 'pref_statut',
298 'pref_etiq_marges_v',
299 'pref_etiq_marges_h',
300 'pref_etiq_hspace',
301 'pref_etiq_vspace',
302 'pref_etiq_hsize',
303 'pref_etiq_vsize',
304 'pref_etiq_cols',
305 'pref_etiq_rows',
306 'pref_etiq_corps',
307 'pref_card_marges_v',
308 'pref_card_marges_h',
309 'pref_card_hspace',
310 'pref_card_vspace'
311 );
312
313 /**
314 * Default constructor
315 *
316 * @param Db $zdb Db instance
317 * @param boolean $load Automatically load preferences on load
318 *
319 * @return void
320 */
321 public function __construct(Db $zdb, bool $load = true)
322 {
323 $this->zdb = $zdb;
324 if ($load) {
325 $this->load();
326 $this->checkUpdate();
327 }
328 }
329
330 /**
331 * Check if all fields referenced in the default array does exists,
332 * create them if not
333 *
334 * @return boolean
335 */
336 private function checkUpdate(): bool
337 {
338 $proceed = false;
339 $params = array();
340 foreach (self::$defaults as $k => $v) {
341 if (!isset($this->prefs[$k])) {
342 if ($k == 'pref_admin_pass' && $v == 'admin') {
343 $v = password_hash($v, PASSWORD_BCRYPT);
344 }
345 $this->prefs[$k] = $v;
346 Analog::log(
347 'The field `' . $k . '` does not exists, Galette will attempt to create it.',
348 Analog::INFO
349 );
350 $proceed = true;
351 $params[] = array(
352 'nom_pref' => $k,
353 'val_pref' => $v
354 );
355 }
356 }
357 if ($proceed !== false) {
358 try {
359 $insert = $this->zdb->insert(self::TABLE);
360 $insert->values(
361 array(
362 'nom_pref' => ':nom_pref',
363 'val_pref' => ':val_pref'
364 )
365 );
366 $stmt = $this->zdb->sql->prepareStatementForSqlObject($insert);
367
368 foreach ($params as $p) {
369 $stmt->execute(
370 array(
371 'nom_pref' => $p['nom_pref'],
372 'val_pref' => $p['val_pref']
373 )
374 );
375 }
376 } catch (Throwable $e) {
377 Analog::log(
378 'Unable to add missing preferences.' . $e->getMessage(),
379 Analog::WARNING
380 );
381 return false;
382 }
383
384 Analog::log(
385 'Missing preferences were successfully stored into database.',
386 Analog::INFO
387 );
388 }
389
390 return true;
391 }
392
393 /**
394 * Load current preferences from database.
395 *
396 * @return boolean
397 */
398 public function load(): bool
399 {
400 $this->prefs = array();
401
402 try {
403 $result = $this->zdb->selectAll(self::TABLE);
404 foreach ($result as $pref) {
405 $this->prefs[$pref->nom_pref] = $pref->val_pref;
406 }
407 $this->socials = Social::getListForMember(null);
408 return true;
409 } catch (Throwable $e) {
410 Analog::log(
411 'Preferences cannot be loaded. Galette should not work without ' .
412 'preferences. Exiting.',
413 Analog::URGENT
414 );
415 return false;
416 }
417 }
418
419 /**
420 * Set default preferences at install time
421 *
422 * @param string $lang language selected at install screen
423 * @param string $adm_login admin login entered at install time
424 * @param string $adm_pass admin password entered at install time
425 *
426 * @return boolean
427 * @throws Throwable
428 */
429 public function installInit(string $lang, string $adm_login, string $adm_pass): bool
430 {
431 try {
432 //first, we drop all values
433 $delete = $this->zdb->delete(self::TABLE);
434 $this->zdb->execute($delete);
435
436 //we then replace default values with the ones user has selected
437 $values = self::$defaults;
438 $values['pref_lang'] = $lang;
439 $values['pref_admin_login'] = $adm_login;
440 $values['pref_admin_pass'] = $adm_pass;
441 $values['pref_card_year'] = date('Y');
442
443 $insert = $this->zdb->insert(self::TABLE);
444 $insert->values(
445 array(
446 'nom_pref' => ':nom_pref',
447 'val_pref' => ':val_pref'
448 )
449 );
450 $stmt = $this->zdb->sql->prepareStatementForSqlObject($insert);
451
452 foreach ($values as $k => $v) {
453 $stmt->execute(
454 array(
455 'nom_pref' => $k,
456 'val_pref' => $v
457 )
458 );
459 }
460
461 Analog::log(
462 'Default preferences were successfully stored into database.',
463 Analog::INFO
464 );
465 return true;
466 } catch (Throwable $e) {
467 Analog::log(
468 'Unable to initialize default preferences.' . $e->getMessage(),
469 Analog::WARNING
470 );
471 throw $e;
472 }
473 }
474
475 /**
476 * Returns all preferences keys
477 *
478 * @return array<string>
479 */
480 public function getFieldsNames(): array
481 {
482 return array_keys($this->prefs);
483 }
484
485 /**
486 * Check values
487 *
488 * @param array<string, mixed> $values Values
489 * @param Login $login Logged in user
490 *
491 * @return boolean
492 */
493 public function check(array $values, Login $login): bool
494 {
495 $insert_values = array();
496 if ($login->isSuperAdmin() && !Galette::isDemo()) {
497 $this->required[] = 'pref_admin_login';
498 }
499
500 // obtain fields
501 foreach ($this->getFieldsNames() as $fieldname) {
502 if (isset($values[$fieldname])) {
503 $value = trim($values[$fieldname]);
504 } else {
505 $value = "";
506 }
507
508 $insert_values[$fieldname] = $value;
509 }
510
511 //cleanup fields for demo
512 if (Galette::isDemo()) {
513 unset(
514 $insert_values['pref_admin_login'],
515 $insert_values['pref_admin_pass'],
516 $insert_values['pref_mail_method']
517 );
518 }
519
520 // missing relations
521 if (
522 !Galette::isDemo()
523 && isset($insert_values['pref_mail_method'])
524 ) {
525 if ($insert_values['pref_mail_method'] > GaletteMail::METHOD_DISABLED) {
526 if (
527 !isset($insert_values['pref_email_nom'])
528 || $insert_values['pref_email_nom'] == ''
529 ) {
530 $this->errors[] = _T("- You must indicate a sender name for emails!");
531 }
532 if (
533 !isset($insert_values['pref_email'])
534 || $insert_values['pref_email'] == ''
535 ) {
536 $this->errors[] = _T("- You must indicate an email address Galette should use to send emails!");
537 }
538 if ($insert_values['pref_mail_method'] == GaletteMail::METHOD_SMTP) {
539 if (
540 !isset($insert_values['pref_mail_smtp_host'])
541 || $insert_values['pref_mail_smtp_host'] == ''
542 ) {
543 $this->errors[] = _T("- You must indicate the SMTP server you want to use!");
544 }
545 }
546 if (
547 $insert_values['pref_mail_method'] == GaletteMail::METHOD_GMAIL
548 || ($insert_values['pref_mail_method'] == GaletteMail::METHOD_SMTP
549 && $insert_values['pref_mail_smtp_auth'])
550 ) {
551 if (
552 !isset($insert_values['pref_mail_smtp_user'])
553 || trim($insert_values['pref_mail_smtp_user']) == ''
554 ) {
555 $this->errors[] = _T("- You must provide a login for SMTP authentication.");
556 }
557 if (
558 !isset($insert_values['pref_mail_smtp_password'])
559 || ($insert_values['pref_mail_smtp_password']) == ''
560 ) {
561 $this->errors[] = _T("- You must provide a password for SMTP authentication.");
562 }
563 }
564 }
565 }
566
567 if (
568 isset($insert_values['pref_beg_membership'])
569 && $insert_values['pref_beg_membership'] != ''
570 && isset($insert_values['pref_membership_ext'])
571 && $insert_values['pref_membership_ext'] != ''
572 ) {
573 $this->errors[] = _T("- Default membership extention and beginning of membership are mutually exclusive.");
574 }
575
576 if (
577 isset($insert_values['pref_membership_offermonths'])
578 && (int)$insert_values['pref_membership_offermonths'] > 0
579 && isset($insert_values['pref_membership_ext'])
580 && $insert_values['pref_membership_ext'] != ''
581 ) {
582 $this->errors[] = _T("- Offering months is only compatible with beginning of membership.");
583 }
584
585 // missing required fields?
586 foreach ($this->required as $val) {
587 if (!isset($values[$val]) || isset($values[$val]) && trim($values[$val]) == '') {
588 $this->errors[] = str_replace(
589 '%field',
590 $val,
591 _T("- Mandatory field %field empty.")
592 );
593 }
594 }
595
596 if (!Galette::isDemo() && isset($values['pref_admin_pass_check'])) {
597 // Check passwords. Hash will be done into the Preferences class
598 if (strcmp($insert_values['pref_admin_pass'], $values['pref_admin_pass_check']) != 0) {
599 $this->errors[] = _T("Passwords mismatch");
600 }
601 }
602
603 //postal address
604 if (isset($insert_values['pref_postal_adress'])) {
605 $value = $insert_values['pref_postal_adress'];
606 if ($value == Preferences::POSTAL_ADDRESS_FROM_PREFS) {
607 if (isset($insert_values['pref_postal_staff_member'])) {
608 unset($insert_values['pref_postal_staff_member']);
609 }
610 } elseif ($value == Preferences::POSTAL_ADDRESS_FROM_STAFF) {
611 if ($value < 1) {
612 $this->errors[] = _T("You have to select a staff member");
613 }
614 }
615 }
616
617 // update preferences
618 foreach ($insert_values as $champ => $valeur) {
619 if (
620 $login->isSuperAdmin()
621 || $champ != 'pref_admin_pass' && $champ != 'pref_admin_login'
622 ) {
623 if (
624 ($champ == "pref_admin_pass" && $_POST['pref_admin_pass'] != '')
625 || ($champ != "pref_admin_pass")
626 ) {
627 $this->$champ = $valeur;
628 }
629 }
630 }
631
632 $this->checkSocials($values);
633
634 return 0 === count($this->errors);
635 }
636
637 /**
638 * Validate value of a field
639 *
640 * @param string $fieldname Field name
641 * @param mixed $value Value to be set
642 *
643 * @return mixed
644 */
645 public function validateValue(string $fieldname, $value)
646 {
647 global $login;
648
649 switch ($fieldname) {
650 case 'pref_email':
651 case 'pref_email_newadh':
652 case 'pref_email_reply_to':
653 //check emails validity
654 //may be a comma separated list of valid emails identifiers:
655 //"The Name <mail@domain.com>,The Other <other@mail.com>" expect for reply_to.
656 $addresses = [];
657 if (trim($value) != '') {
658 if ($fieldname == 'pref_email_newadh') {
659 $addresses = explode(',', $value);
660 } else {
661 $addresses = [$value];
662 }
663 }
664 foreach ($addresses as $address) {
665 if (!GaletteMail::isValidEmail($address)) {
666 $msg = str_replace('%s', $address, _T("Invalid E-Mail address: %s"));
667 Analog::log($msg, Analog::WARNING);
668 $this->errors[] = $msg;
669 }
670 }
671 break;
672 case 'pref_admin_login':
673 if (Galette::isDemo()) {
674 Analog::log(
675 'Trying to set superadmin login while in DEMO.',
676 Analog::WARNING
677 );
678 } else {
679 if (strlen($value) < 4) {
680 $this->errors[] = _T("- The username must be composed of at least 4 characters!");
681 } else {
682 //check if login is already taken
683 if ($login->loginExists($value)) {
684 $this->errors[] = _T("- This username is already used by another member !");
685 }
686 }
687 }
688 break;
689 case 'pref_numrows':
690 case 'pref_etiq_marges_h':
691 case 'pref_etiq_marges_v':
692 case 'pref_etiq_hspace':
693 case 'pref_etiq_vspace':
694 case 'pref_etiq_hsize':
695 case 'pref_etiq_vsize':
696 case 'pref_etiq_cols':
697 case 'pref_etiq_rows':
698 case 'pref_etiq_corps':
699 case 'pref_card_marges_v':
700 case 'pref_card_marges_h':
701 case 'pref_card_hspace':
702 case 'pref_card_vspace':
703 if (!is_numeric($value) || $value < 0) {
704 $this->errors[] = _T("- The numbers and measures have to be integers!");
705 }
706 break;
707 case 'pref_card_tcol':
708 case 'pref_card_scol':
709 case 'pref_card_bcol':
710 case 'pref_card_hcol':
711 $matches = [];
712 if (!preg_match("/^(#)?([0-9A-F]{6})$/i", $value, $matches)) {
713 // Set strip background colors to black or white (for tcol)
714 $value = ($fieldname == 'pref_card_tcol' ? '#FFFFFF' : '#000000');
715 } else {
716 $value = '#' . $matches[2];
717 }
718 break;
719 case 'pref_admin_pass':
720 if (Galette::isDemo()) {
721 Analog::log(
722 'Trying to set superadmin pass while in DEMO.',
723 Analog::WARNING
724 );
725 } else {
726 $pwcheck = new \Galette\Util\Password($this);
727 $pwcheck->addPersonalInformation([$this->pref_admin_login]);
728 if (!$pwcheck->isValid($value)) {
729 $this->errors = array_merge(
730 $this->errors,
731 $pwcheck->getErrors()
732 );
733 }
734 }
735 break;
736 case 'pref_membership_ext':
737 if (!is_numeric($value) || $value < 0) {
738 $this->errors[] = _T("- Invalid number of months of membership extension.");
739 }
740 break;
741 case 'pref_beg_membership':
742 $beg_membership = explode("/", $value);
743 if (count($beg_membership) != 2) {
744 $this->errors[] = _T("- Invalid format of beginning of membership.");
745 } else {
746 $now = getdate();
747 if (!checkdate((int)$beg_membership[1], (int)$beg_membership[0], $now['year'])) {
748 $this->errors[] = _T("- Invalid date for beginning of membership.");
749 }
750 }
751 break;
752 case 'pref_membership_offermonths':
753 if (!is_numeric($value) || $value < 0) {
754 $this->errors[] = _T("- Invalid number of offered months.");
755 }
756 break;
757 case 'pref_card_year':
758 if ($value !== 'DEADLINE' && !preg_match('/^(?:\d{4}|\d{2})(\D?)(?:\d{4}|\d{2})$/', $value)) {
759 $this->errors[] = _T("- Invalid year for cards.");
760 }
761 break;
762 case 'pref_footer':
763 $value = $this->cleanHtmlValue($value);
764 break;
765 case 'pref_website':
766 if (!isValidWebUrl($value)) {
767 $this->errors[] = _T("- Invalid website URL.");
768 }
769 break;
770 }
771
772 return $value;
773 }
774
775 /**
776 * Will store all preferences in the database
777 *
778 * @return boolean
779 */
780 public function store(): bool
781 {
782 try {
783 $this->zdb->connection->beginTransaction();
784 $update = $this->zdb->update(self::TABLE);
785 $update->set(
786 array(
787 'val_pref' => ':val_pref'
788 )
789 )->where->equalTo('nom_pref', ':nom_pref');
790
791 $stmt = $this->zdb->sql->prepareStatementForSqlObject($update);
792
793 foreach (self::$defaults as $k => $v) {
794 if (
795 Galette::isDemo()
796 && in_array($k, ['pref_admin_pass', 'pref_admin_login', 'pref_mail_method'])
797 ) {
798 continue;
799 }
800 Analog::log('Storing ' . $k, Analog::DEBUG);
801
802 $value = $this->prefs[$k];
803 //do not store pdf_adhesion_form, it's designed to be overriden by plugin
804 if ($k === 'pref_adhesion_form') {
805 if (trim($v) == '') {
806 //Reset to default, should not be empty
807 $v = self::$defaults['pref_adhesion_form'];
808 }
809 $value = $v;
810 }
811
812 $stmt->execute(
813 array(
814 'val_pref' => $value,
815 'nom_pref' => $k
816 )
817 );
818 }
819 $this->zdb->connection->commit();
820 Analog::log(
821 'Preferences were successfully stored into database.',
822 Analog::INFO
823 );
824
825 $this->storeSocials(null);
826
827 return true;
828 } catch (Throwable $e) {
829 $this->zdb->connection->rollBack();
830
831 $messages = array();
832 do {
833 $messages[] = $e->getMessage();
834 } while ($e = $e->getPrevious());
835
836 Analog::log(
837 'Unable to store preferences | ' . print_r($messages, true),
838 Analog::WARNING
839 );
840 return false;
841 }
842 }
843
844 /**
845 * Returns postal address
846 *
847 * @return string postal address
848 */
849 public function getPostalAddress(): string
850 {
851 $regs = array(
852 '/%name/',
853 '/%complement/',
854 '/%address/',
855 '/%zip/',
856 '/%town/',
857 '/%country/',
858 );
859
860 $replacements = null;
861
862 if ($this->prefs['pref_postal_adress'] == self::POSTAL_ADDRESS_FROM_PREFS) {
863 $_address = $this->prefs['pref_adresse'];
864 if ($this->prefs['pref_adresse2'] && $this->prefs['pref_adresse2'] != '') {
865 $_address .= "\n" . $this->prefs['pref_adresse2'];
866 }
867 $replacements = array(
868 $this->prefs['pref_nom'],
869 "\n",
870 $_address,
871 $this->prefs['pref_cp'],
872 $this->prefs['pref_ville'],
873 $this->prefs['pref_pays']
874 );
875 } else {
876 //get selected staff member address
877 $adh = new Adherent($this->zdb, (int)$this->prefs['pref_postal_staff_member']);
878 $_complement = preg_replace(
879 array('/%name/', '/%status/'),
880 array($this->prefs['pref_nom'], $adh->sstatus),
881 _T("%name association's %status")
882 ) . "\n";
883 $_address = $adh->address;
884
885 $replacements = array(
886 $adh->sfullname . "\n",
887 $_complement,
888 $_address,
889 $adh->zipcode,
890 $adh->town,
891 $adh->country
892 );
893 }
894
895 /*FIXME: i18n fails :/ */
896 /*$r = preg_replace(
897 $regs,
898 $replacements,
899 _T("%name\n%complement\n%address\n%zip %town - %country")
900 );*/
901 $r = preg_replace(
902 $regs,
903 $replacements,
904 "%name%complement%address\n%zip %town - %country"
905 );
906 return $r;
907 }
908
909 /**
910 * Are public pages visible?
911 *
912 * @param Authentication $login Authentication instance
913 *
914 * @return boolean
915 */
916 public function showPublicPages(Authentication $login): bool
917 {
918 if ($this->prefs['pref_bool_publicpages']) {
919 //if public pages are actives, let's check if we
920 //display them for curent call
921 switch ($this->prefs['pref_publicpages_visibility']) {
922 case self::PUBLIC_PAGES_VISIBILITY_PUBLIC:
923 //pages are publically visibles
924 return true;
925 case self::PUBLIC_PAGES_VISIBILITY_RESTRICTED:
926 //pages should be displayed only for up-to-date members
927 if (
928 $login->isUp2Date()
929 || $login->isAdmin()
930 || $login->isStaff()
931 ) {
932 return true;
933 } else {
934 return false;
935 }
936 case self::PUBLIC_PAGES_VISIBILITY_PRIVATE:
937 //pages should be displayed only for staff and admins
938 if ($login->isAdmin() || $login->isStaff()) {
939 return true;
940 } else {
941 return false;
942 }
943 default:
944 //should never be there
945 return false;
946 }
947 } else {
948 return false;
949 }
950 }
951
952 /**
953 * Global getter method
954 *
955 * @param string $name name of the property we want to retrieve
956 *
957 * @return mixed the called property
958 */
959 public function __get(string $name)
960 {
961 $forbidden = array('defaults');
962 $virtuals = array('vpref_email', 'vpref_email_newadh');
963
964 if (!in_array($name, $forbidden) && isset($this->prefs[$name])) {
965 if (
966 Galette::isDemo()
967 && $name == 'pref_mail_method'
968 ) {
969 return GaletteMail::METHOD_DISABLED;
970 } elseif ($name == 'pref_footer') {
971 return $this->cleanHtmlValue($this->prefs[$name]);
972 } else {
973 if ($name == 'pref_adhesion_form' && $this->prefs[$name] == '') {
974 $this->prefs[$name] = self::$defaults['pref_adhesion_form'];
975 }
976 $value = $this->prefs[$name];
977 if (TYPE_DB === \Galette\Core\Db::PGSQL) {
978 if ($value === 'f') {
979 $value = false;
980 }
981 }
982
983 if (in_array($name, ['vpref_email', 'pref_email_newadh'])) {
984 $values = explode(',', $value);
985 $value = $values[0]; //take first as default
986 }
987
988 return $value;
989 }
990 } elseif (in_array($name, $virtuals)) {
991 $virtual = str_replace('vpref_', 'pref_', $name);
992 return explode(',', $this->prefs[$virtual]);
993 } elseif ($name === 'socials') {
994 return $this->socials;
995 } else {
996 Analog::log(
997 'Preference `' . $name . '` is not set or is forbidden',
998 Analog::INFO
999 );
1000 return false;
1001 }
1002 }
1003
1004 /**
1005 * Global isset method
1006 * Required for twig to access properties via __get
1007 *
1008 * @param string $name name of the property we want to retrieve
1009 *
1010 * @return bool
1011 */
1012 public function __isset(string $name): bool
1013 {
1014 $forbidden = array('defaults');
1015 $virtuals = array('vpref_email', 'vpref_email_newadh');
1016
1017 if (!in_array($name, $forbidden) && isset($this->prefs[$name])) {
1018 return true;
1019 } elseif (in_array($name, $virtuals)) {
1020 return true;
1021 } elseif ($name === 'socials') {
1022 return true;
1023 } else {
1024 Analog::log(
1025 'Preference `' . $name . '` is not set or is forbidden',
1026 Analog::INFO
1027 );
1028 return false;
1029 }
1030 }
1031
1032 /**
1033 * Get default preferences
1034 *
1035 * @return array<string, mixed>
1036 */
1037 public function getDefaults(): array
1038 {
1039 return self::$defaults;
1040 }
1041
1042 /**
1043 * Global setter method
1044 *
1045 * @param string $name name of the property we want to assign a value to
1046 * @param mixed $value a relevant value for the property
1047 *
1048 * @return void
1049 */
1050 public function __set(string $name, $value): void
1051 {
1052 //does this pref exists ?
1053 if (!array_key_exists($name, self::$defaults)) {
1054 Analog::log(
1055 'Trying to set a preference value which does not seem to exist ('
1056 . $name . ')',
1057 Analog::WARNING
1058 );
1059 return;
1060 }
1061
1062 if (
1063 $name == 'pref_email'
1064 || $name == 'pref_email_newadh'
1065 || $name == 'pref_email_reply_to'
1066 ) {
1067 if (Galette::isDemo()) {
1068 Analog::log(
1069 'Trying to set pref_email while in DEMO.',
1070 Analog::WARNING
1071 );
1072 return;
1073 }
1074 }
1075
1076 // now, check validity
1077 if ($value != '') {
1078 $value = $this->validateValue($name, $value);
1079 }
1080
1081 //some values need to be changed (eg. passwords)
1082 if ($name == 'pref_admin_pass') {
1083 $value = password_hash($value, PASSWORD_BCRYPT);
1084 }
1085
1086 //okay, let's update value
1087 $this->prefs[$name] = $value;
1088 }
1089
1090 /**
1091 * Get instance URL from configuration (if set) or guessed if not
1092 *
1093 * @return string
1094 */
1095 public function getURL(): string
1096 {
1097 $url = null;
1098 if (isset($this->prefs['pref_galette_url']) && !empty($this->prefs['pref_galette_url'])) {
1099 $url = $this->prefs['pref_galette_url'];
1100 } else {
1101 $url = $this->getDefaultURL();
1102 }
1103 return $url;
1104 }
1105
1106 /**
1107 * Get default URL (when not set by user in preferences)
1108 *
1109 * @return string
1110 */
1111 public function getDefaultURL(): string
1112 {
1113 if (defined('GALETTE_CRON')) {
1114 if (defined('GALETTE_URI')) {
1115 return GALETTE_URI;
1116 } else {
1117 throw new \RuntimeException(_T('Please define constant "GALETTE_URI" with the path to your instance.'));
1118 }
1119 }
1120
1121 $scheme = (isset($_SERVER['HTTPS']) ? 'https' : 'http');
1122 $uri = $scheme . '://' . $_SERVER['HTTP_HOST'];
1123 return $uri;
1124 }
1125
1126 /**
1127 * Get last telemetry date
1128 *
1129 * @return string
1130 */
1131 public function getTelemetryDate(): string
1132 {
1133 $rawdate = $this->prefs['pref_telemetry_date'];
1134 if ($rawdate) {
1135 $date = new \DateTime($rawdate);
1136 return $date->format(_T('Y-m-d H:i:s'));
1137 } else {
1138 return _T('Never');
1139 }
1140 }
1141
1142 /**
1143 * Get last telemetry registration date
1144 *
1145 * @return string|null
1146 */
1147 public function getRegistrationDate(): ?string
1148 {
1149 $rawdate = $this->prefs['pref_registration_date'];
1150 if ($rawdate) {
1151 $date = new \DateTime($rawdate);
1152 return $date->format(_T('Y-m-d H:i:s'));
1153 }
1154
1155 return null;
1156 }
1157
1158 /**
1159 * Check member cards sizes
1160 * Always a A4/portrait
1161 *
1162 * @return array<string>
1163 */
1164 public function checkCardsSizes(): array
1165 {
1166 $warning_detected = [];
1167 //check page width
1168 $max = 210;
1169 //margins
1170 $size = $this->pref_card_marges_h * 2;
1171 //cards
1172 $size += PdfMembersCards::getWidth() * PdfMembersCards::getCols();
1173 //spacing
1174 $size += $this->pref_card_hspace * (PdfMembersCards::getCols() - 1);
1175 if ($size > $max) {
1176 $warning_detected[] = _T('Current cards configuration may exceed page width!');
1177 }
1178
1179 $max = 297;
1180 //margins
1181 $size = $this->pref_card_marges_v * 2;
1182 //cards
1183 $size += PdfMembersCards::getHeight() * PdfMembersCards::getRows();
1184 //spacing
1185 $size += $this->pref_card_vspace * (PdfMembersCards::getRows() - 1);
1186 if ($size > $max) {
1187 $warning_detected[] = _T('Current cards configuration may exceed page height!');
1188 }
1189
1190 return $warning_detected;
1191 }
1192
1193 /**
1194 * Get errors
1195 *
1196 * @return array<string>
1197 */
1198 public function getErrors(): array
1199 {
1200 return $this->errors;
1201 }
1202
1203 /**
1204 * Build legend array
1205 *
1206 * @return array<string, array<string, string>>
1207 */
1208 public function getLegend(): array
1209 {
1210 $legend = [];
1211
1212 $legend['main'] = [
1213 'title' => _T('Main information'),
1214 'patterns' => $this->getMainPatterns()
1215 ];
1216
1217 $s_patterns = $this->getSignaturePatterns(false);
1218 if (count($s_patterns)) {
1219 $legend['socials'] = [
1220 'title' => _T('Social networks'),
1221 'patterns' => $this->getSignaturePatterns(false)
1222 ];
1223 }
1224
1225 return $legend;
1226 }
1227
1228 /**
1229 * Get email signature
1230 *
1231 * @param PHPMailer $mail PHPMailer instance
1232 *
1233 * @return string
1234 */
1235 public function getMailSignature(PHPMailer $mail): string
1236 {
1237 global $routeparser;
1238
1239 $signature = $this->pref_mail_sign;
1240
1241 if (trim($signature) == '') {
1242 return '';
1243 }
1244
1245 $this->setPreferences($this)->setRouteparser($routeparser);
1246 $this->setPatterns(
1247 $this->getMainPatterns() + $this->getSignaturePatterns()
1248 );
1249 $this
1250 ->setMail($mail)
1251 ->setMain()
1252 ->setSocialReplacements();
1253
1254 $signature = $this->proceedReplacements($signature);
1255
1256 return "\r\n-- \r\n" . $signature;
1257 }
1258
1259 /**
1260 * Get patterns for mail signature
1261 *
1262 * @param boolean $legacy Whether to load legacy patterns
1263 *
1264 * @return array<string, array<string, string>>
1265 */
1266 protected function getSignaturePatterns(bool $legacy = true): array
1267 {
1268 $s_patterns = [];
1269 $social = new Social($this->zdb);
1270
1271 $types = $this->getCoreRegisteredTypes() + $social->getSystemTypes(false);
1272
1273 foreach ($types as $type) {
1274 $s_patterns['asso_social_' . strtolower($type)] = [
1275 'title' => $social->getSystemType($type),
1276 'pattern' => '/{ASSO_SOCIAL_' . strtoupper($type) . '}/'
1277 ];
1278 }
1279
1280 if ($legacy === true) {
1281 $main = $this->getMainPatterns();
1282 $s_patterns['_asso_name'] = [
1283 'title' => $main['asso_name']['title'],
1284 'pattern' => '/{NAME}/'
1285 ];
1286
1287 $s_patterns['_asso_website'] = [
1288 'title' => $main['asso_website']['title'],
1289 'pattern' => '/{WEBSITE}/'
1290 ];
1291
1292 foreach ([Social::FACEBOOK, Social::TWITTER, Social::LINKEDIN, Social::VIADEO] as $legacy_type) {
1293 $s_patterns['_asso_social_' . $legacy_type] = [
1294 'title' => $s_patterns['asso_social_' . $legacy_type]['title'],
1295 'pattern' => '/{' . strtoupper($legacy_type) . '}/'
1296 ];
1297 }
1298 }
1299
1300 return $s_patterns;
1301 }
1302
1303 /**
1304 * Set emails replacements
1305 *
1306 * @return $this
1307 */
1308 public function setSocialReplacements(): self
1309 {
1310 $replacements = [];
1311
1312 $done_replacements = $this->getReplacements();
1313 $replacements['_asso_name'] = $done_replacements['asso_name'];
1314 $replacements['asso_website'] = $this->pref_website;
1315 $replacements['_asso_website'] = $replacements['asso_website'];
1316
1317 $social = new Social($this->zdb);
1318 $types = $this->getCoreRegisteredTypes() + $social->getSystemTypes(false);
1319
1320 foreach ($types as $type) {
1321 $replace_value = null;
1322 $socials = Social::getListForMember(null, $type);
1323 if (count($socials)) {
1324 $replace_value = '';
1325 foreach ($socials as $social) {
1326 if ($replace_value != '') {
1327 $replace_value .= ', ';
1328 }
1329 $replace_value .= $social->url;
1330 }
1331 }
1332 $replacements['asso_social_' . strtolower($type)] = $replace_value;
1333 }
1334
1335
1336 foreach ([Social::FACEBOOK, Social::TWITTER, Social::LINKEDIN, Social::VIADEO] as $legacy_type) {
1337 $replacements['_asso_social_' . $legacy_type] = $replacements['asso_social_' . $legacy_type];
1338 }
1339
1340 $this->setReplacements($replacements);
1341
1342 return $this;
1343 }
1344
1345 /**
1346 * Purify HTML value
1347 *
1348 * @param string $value Value to clean
1349 *
1350 * @return string
1351 */
1352 public function cleanHtmlValue(string $value): string
1353 {
1354 $config = \HTMLPurifier_Config::createDefault();
1355 $cache_dir = rtrim(GALETTE_CACHE_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'htmlpurifier';
1356 if (!file_exists($cache_dir)) {
1357 mkdir($cache_dir, 0755, true);
1358 }
1359 $config->set('Cache.SerializerPath', $cache_dir);
1360 $purifier = new \HTMLPurifier($config);
1361 return $purifier->purify($value);
1362 }
1363 }