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