3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
10 * Copyright © 2020-2023 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
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.
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.
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/>.
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2020-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.eu
37 namespace Galette\Features
;
40 use Galette\Core\Login
;
41 use Galette\Core\Logo
;
42 use Galette\Core\Preferences
;
43 use Galette\DynamicFields\Choice
;
44 use Galette\DynamicFields\Separator
;
45 use Galette\Entity\Adherent
;
46 use Galette\Entity\Contribution
;
47 use Galette\Entity\PdfModel
;
48 use Galette\Entity\Reminder
;
49 use Galette\Entity\Texts
;
50 use Galette\Repository\DynamicFieldsSet
;
51 use Galette\DynamicFields\DynamicField
;
54 use PHPMailer\PHPMailer\PHPMailer
;
55 use Slim\Routing\RouteParser
;
56 use DI\Attribute\Inject
;
59 * Replacements feature
64 * @author Johan Cwiklinski <johan@x-tnd.be>
65 * @copyright 2020-2023 The Galette Team
66 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
67 * @link http://galette.eu
73 private $patterns = [];
74 private $replaces = [];
75 private $dynamic_patterns = [];
76 private ?PHPMailer
$mail = null;
93 #[Inject("preferences")]
94 protected $preferences;
99 protected $routeparser;
102 * Get dynamic patterns
104 * @param string $form_name Dynamic form name
105 * @param boolean $legacy Whether to load legacy patterns
109 public function getDynamicPatterns(string $form_name, bool $legacy = true): array
111 $fields = new DynamicFieldsSet($this->zdb
, $this->login
);
112 $dynamic_fields = $fields->getList($form_name);
114 $dynamic_patterns = [];
115 foreach ($dynamic_fields as $dynamic_field) {
116 //no pattern for separators
117 if ($dynamic_field instanceof Separator
) {
120 $key = strtoupper('DYNFIELD_' . $dynamic_field->getId() . '_' . $form_name);
125 if (!($this instanceof Texts
) && ($legacy === true ||
$dynamic_field instanceof Choice
)) {
126 $capabilities[] = 'INPUT';
128 foreach ($capabilities as $capability) {
129 $skey = sprintf('%s_%s', $capability, $key);
130 switch ($capability) {
132 $title = _T('Label for dynamic field "%s"');
135 $title = _T('Form entry for dynamic field "%s"');
141 $title = _T('Value for dynamic field "%s"');
144 $dynamic_patterns[strtolower($skey)] = [
147 $dynamic_field->getName()
149 'pattern' => sprintf('/{%s}/', $skey)
154 $this->dynamic_patterns
[$form_name] = $dynamic_patterns;
155 return $this->dynamic_patterns
[$form_name];
161 * @param array $patterns Patterns to add
165 protected function setPatterns(array $patterns): self
168 foreach ($patterns as $key => $info) {
169 if (is_array($info)) {
170 $toset[$key] = $info['pattern'];
172 $toset[$key] = $info;
176 $this->patterns
= array_merge(
187 * @param array $replaces Replacements to add
191 public function setReplacements(array $replaces): void
193 $this->replaces
= array_merge(
204 protected function getMainPatterns(): array
208 'title' => _T('Your organisation name'),
209 'pattern' => '/{ASSO_NAME}/'
212 'title' => _T('Your organisation slogan'),
213 'pattern' => '/{ASSO_SLOGAN}/'
216 'title' => _T('Your organisation address'),
217 'pattern' => '/{ASSO_ADDRESS}/',
219 'asso_address_multi' => [
220 'title' => sprintf('%s (%s)', _T('Your organisation address'), _T('with break lines')),
221 'pattern' => '/{ASSO_ADDRESS_MULTI}/',
224 'title' => _T('Your organisation website'),
225 'pattern' => '/{ASSO_WEBSITE}/',
228 'title' => _T('Your organisation logo'),
229 'pattern' => '/{ASSO_LOGO}/',
232 //TRANS: see https://www.php.net/manual/datetime.format.php
233 'title' => _T('Current date (Y-m-d)'),
234 'pattern' => '/{DATE_NOW}/'
237 'title' => _T("Galette's login URI"),
238 'pattern' => '/{LOGIN_URI}/'
244 * Get patterns for a member
246 * @param boolean $legacy Whether to load legacy patterns
250 protected function getMemberPatterns(bool $legacy = true): array
252 $dynamic_patterns = $this->getDynamicPatterns('adh', $legacy);
255 'title' => _('Title'),
256 'pattern' => '/{TITLE_ADH}/',
259 'title' => _T("Member's ID"),
260 'pattern' => '/{ID_ADH}/',
263 'title' => _T("Member number"),
264 'pattern' => '/{NUM_ADH}/',
267 'title' => _T("Name"),
268 'pattern' => '/{NAME_ADH}/',
271 'title' => _T('Last name'),
272 'pattern' => '/{LAST_NAME_ADH}/',
274 'adh_first_name' => [
275 'title' => _T('First name'),
276 'pattern' => '/{FIRST_NAME_ADH}/',
279 'title' => _T('Nickname'),
280 'pattern' => '/{NICKNAME_ADH}/',
283 'title' => _T('Gender'),
284 'pattern' => '/{GENDER_ADH}/',
286 'adh_birth_date' => [
287 'title' => _T('Birth date'),
288 'pattern' => '/{ADH_BIRTH_DATE}/',
290 'adh_birth_place' => [
291 'title' => _T('Birth place'),
292 'pattern' => '/{ADH_BIRTH_PLACE}/',
294 'adh_profession' => [
295 'title' => _T('Profession'),
296 'pattern' => '/{PROFESSION_ADH}/',
299 'title' => _T("Company name"),
300 'pattern' => '/{COMPANY_ADH}/',
303 'title' => _T("Address"),
304 'pattern' => '/{ADDRESS_ADH}/',
306 'adh_address_multi' => [
307 'title' => sprintf('%s (%s)', _T('Address'), _T('with break lines')),
308 'pattern' => '/{ADDRESS_ADH_MULTI}/',
311 'title' => _T("Zipcode"),
312 'pattern' => '/{ZIP_ADH}/',
315 'title' => _T("Town"),
316 'pattern' => '/{TOWN_ADH}/',
319 'title' => _T('Country'),
320 'pattern' => '/{COUNTRY_ADH}/',
323 'title' => _T('Phone'),
324 'pattern' => '/{PHONE_ADH}/',
327 'title' => _T('GSM'),
328 'pattern' => '/{MOBILE_ADH}/',
331 'title' => _T('Email'),
332 'pattern' => '/{EMAIL_ADH}/',
335 'title' => _T('Login'),
336 'pattern' => '/{LOGIN_ADH}/',
338 'adh_main_group' => [
339 'title' => _T("Member's main group"),
340 'pattern' => '/{GROUP_ADH}/',
343 'title' => _T("Member's groups (as list)"),
344 'pattern' => '/{GROUPS_ADH}/'
347 'title' => _T('Member state of dues'),
348 'pattern' => '/{ADH_DUES}/'
350 'days_remaining' => [
351 'title' => _T('Membership remaining days'),
352 'pattern' => '/{DAYS_REMAINING}/',
355 'title' => _T('Membership expired since'),
356 'pattern' => '/{DAYS_EXPIRED}/',
360 if ($legacy === true) {
363 'title' => _T("Company name"),
364 'pattern' => '/{COMPANY_NAME_ADH}/',
366 '_adh_last_name' => [
367 'title' => _T('Last name'),
368 'pattern' => '/{LASTNAME_ADH}/',
370 '_adh_first_name' => [
371 'title' => _T('First name'),
372 'pattern' => '/{FIRSTNAME_ADH}/',
375 'title' => _T('Login'),
376 'pattern' => '/{LOGIN}/',
379 'title' => _T('Email'),
380 'pattern' => '/{MAIL_ADH}/',
385 return $m_patterns +
$dynamic_patterns;
389 * Get patterns for a contribution
391 * @param boolean $legacy Whether to load legacy patterns
395 protected function getContributionPatterns($legacy = true): array
397 $dynamic_patterns = $this->getDynamicPatterns('contrib', $legacy);
401 'title' => _T('Contribution label'),
402 'pattern' => '/{CONTRIB_LABEL}/',
404 'contrib_amount' => [
405 'title' => _T('Amount'),
406 'pattern' => '/{CONTRIB_AMOUNT}/',
408 'contrib_amount_letters' => [
409 'title' => _T('Amount (in letters)'),
410 'pattern' => '/{CONTRIB_AMOUNT_LETTERS}/',
413 'title' => _T('Full date'),
414 'pattern' => '/{CONTRIB_DATE}/',
417 'title' => _T('Contribution year'),
418 'pattern' => '/{CONTRIB_YEAR}/',
420 'contrib_comment' => [
421 'title' => _T('Comment'),
422 'pattern' => '/{CONTRIB_COMMENT}/',
425 'title' => _T('Begin date'),
426 'pattern' => '/{CONTRIB_BEGIN_DATE}/',
429 'title' => _T('End date'),
430 'pattern' => '/{CONTRIB_END_DATE}/',
433 'title' => _T('Contribution id'),
434 'pattern' => '/{CONTRIB_ID}/',
436 'contrib_payment' => [
437 'title' => _T('Payment type'),
438 'pattern' => '/{CONTRIB_PAYMENT_TYPE}/'
441 'title' => _T('Contribution information'),
442 'pattern' => '/{CONTRIB_INFO}/'
446 if ($legacy === true) {
447 foreach ($c_patterns as $key => $pattern) {
449 $pattern['pattern'] = str_replace(
454 $c_patterns[$nkey] = $pattern;
457 $c_patterns['__contrib_label'] = [
458 'title' => $c_patterns['contrib_label']['title'],
459 'pattern' => '/{CONTRIB_TYPE}/'
463 //handle DEADLINE alias
464 $c_patterns['deadline'] = [
465 'title' => $c_patterns['contrib_edate']['title'],
466 'pattern' => '/{DEADLINE}/'
469 return $c_patterns +
$dynamic_patterns;
475 * @param PHPMailer $mail PHPMailer instance
479 public function setMail(PHPMailer
$mail): self
486 * Set main replacements
490 public function setMain(): self
492 $address = $this->preferences
->getPostalAddress();
493 $address_multi = preg_replace("/\n/", "<br>", $address);
496 if ($this->preferences
->pref_website
!== '') {
497 $website = '<a href="' . $this->preferences
->pref_website
. '">' .
498 $this->preferences
->pref_website
. '</a>';
502 if ($this->mail
!== null) {
503 $logo_content = $this->preferences
->getURL() . $this->routeparser
->urlFor('logo');
505 $logo_content = '@' . base64_encode(file_get_contents($logo->getPath()));
508 '<img src="%1$s" width="%2$s" height="%3$s" alt="" />',
510 $logo->getOptimalWidth(),
511 $logo->getOptimalHeight()
514 $this->setReplacements(
516 'asso_name' => $this->preferences
->pref_nom
,
517 'asso_slogan' => $this->preferences
->pref_slogan
,
518 'asso_address' => $address,
519 'asso_address_multi' => $address_multi,
520 'asso_website' => $website,
521 'asso_logo' => $logo_elt,
522 //TRANS: see https://www.php.net/manual/datetime.format.php
523 'date_now' => date(_T('Y-m-d')),
524 'login_uri' => $this->preferences
->getURL() . $this->routeparser
->urlFor('login'),
532 * Set contribution and proceed related replacements
536 public function setNoContribution(): self
541 'contrib_label' => null,
542 'contrib_amount' => null,
543 'contrib_amount_letters' => null,
544 'contrib_date' => null,
545 'contrib_year' => null,
546 'contrib_comment' => null,
547 'contrib_bdate' => null,
548 'contrib_edate' => null,
549 'contrib_id' => null,
550 'contrib_payment' => null,
551 'contrib_info' => null
554 foreach ($c_replacements as $key => $replacement) {
556 $c_replacements[$nkey] = $replacement;
558 $c_replacements['__contrib_label'] = $c_replacements['contrib_label'];
560 //handle DEADLINE alias
561 $c_replacements['deadline'] = null;
563 $this->setReplacements($c_replacements);
565 /** the list of all dynamic fields */
566 $fields = new DynamicFieldsSet($this->zdb
, $login);
567 $dynamic_fields = $fields->getList('contrib');
568 $this->setDynamicFields('contrib', $dynamic_fields, null);
574 * Set contribution and proceed related replacements
576 * @param Contribution $contrib Contribution
580 public function setContribution(Contribution
$contrib): self
582 global $login, $i18n;
584 $formatter = new NumberFormatter($i18n->getID(), NumberFormatter
::SPELLOUT
);
587 'contrib_label' => $contrib->type
->libelle
,
588 'contrib_amount' => $contrib->amount
,
589 'contrib_amount_letters' => $formatter->format($contrib->amount
),
590 'contrib_date' => $contrib->date
,
591 'contrib_year' => $contrib->raw_date
->format('Y'),
592 'contrib_comment' => $contrib->info
,
593 'contrib_bdate' => $contrib->begin_date
,
594 'contrib_edate' => $contrib->end_date
,
595 'contrib_id' => $contrib->id
,
596 'contrib_payment' => $contrib->spayment_type
,
597 'contrib_info' => $contrib->info
600 foreach ($c_replacements as $key => $replacement) {
602 $c_replacements[$nkey] = $replacement;
604 $c_replacements['__contrib_label'] = $c_replacements['contrib_label'];
606 //handle DEADLINE alias
607 $c_replacements['deadline'] = $c_replacements['contrib_edate'];
609 $this->setReplacements($c_replacements);
611 /** the list of all dynamic fields */
612 $fields = new DynamicFieldsSet($this->zdb
, $login);
613 $dynamic_fields = $fields->getList('contrib');
614 $this->setDynamicFields('contrib', $dynamic_fields, $contrib);
620 * Set member and proceed related replacements
622 * @param Adherent $member Member
626 public function setMember(Adherent
$member): self
630 $address = $member->getAddress();
631 $address_multi = preg_replace("/\n/", "<br>", $address);
633 if ($member->isMan()) {
635 } elseif ($member->isWoman()) {
636 $gender = _T("Woman");
638 $gender = _T("Unspecified");
641 $member_groups = $member->groups
;
642 $main_group = _T("None");
643 $group_list = _T("None");
644 if (is_array($member_groups) && count($member_groups) > 0) {
645 $main_group = $member_groups[0]->getName();
646 $group_list = '<ul>';
647 foreach ($member_groups as $group) {
648 $group_list .= '<li>' . $group->getName() . '</li>';
650 $group_list .= '</ul>';
653 $this->setReplacements(
655 'adh_title' => $member->stitle
,
656 'adh_id' => $member->id
,
657 'adh_num' => $member->number
,
658 'adh_name' => $member->sfullname
,
659 'adh_last_name' => $member->name
,
660 'adh_first_name' => $member->surname
,
661 'adh_nickname' => $member->nickname
,
662 'adh_gender' => $gender,
663 'adh_birth_date' => $member->birthdate
,
664 'adh_birth_place' => $member->birth_place
,
665 'adh_profession' => $member->job
,
666 'adh_company' => $member->company_name
,
667 'adh_address' => $address,
668 'adh_address_multi' => $address_multi,
669 'adh_zip' => $member->getZipcode(),
670 'adh_town' => $member->getTown(),
671 'adh_country' => $member->getCountry(),
672 'adh_phone' => $member->phone
,
673 'adh_mobile' => $member->gsm
,
674 //always take current member email, to be sure.
675 'adh_email' => $member->email
,
676 'adh_login' => $member->login
,
677 'adh_main_group' => $main_group,
678 'adh_groups' => $group_list,
679 'adh_dues' => $member->getDues(),
680 'days_remaining' => $member->days_remaining
,
681 'days_expired' => (int)$member->days_remaining +
1,
682 //Handle COMPANY_NAME_ADH... https://bugs.galette.eu/issues/1530
683 '_adh_company' => $member->company_name
,
684 //Handle old names for variables ... https://bugs.galette.eu/issues/1393
685 '_adh_last_name' => $member->name
,
686 '_adh_first_name' => $member->surname
,
687 '_adh_login' => $member->login
,
688 '_adh_email' => $member->email
692 /** the list of all dynamic fields */
693 $fields = new DynamicFieldsSet($this->zdb
, $login);
694 $dynamic_fields = $fields->getList('adh');
695 $this->setDynamicFields('adh', $dynamic_fields, $member);
701 * Set dynamic fields and proceed related replacements
703 * @param string $form_name Form name
704 * @param array $dynamic_fields Dynamic fields
705 * @param mixed $object Related object (Adherent, Contribution, ...)
709 public function setDynamicFields(string $form_name, array $dynamic_fields, $object): self
711 $uform_name = strtoupper($form_name);
713 $dynamic_patterns = $this->getDynamicPatterns($form_name);
714 foreach ($dynamic_patterns as $dynamic_pattern) {
715 $pattern = trim($dynamic_pattern['pattern'], '/');
716 $key = strtolower(rtrim(ltrim($pattern, '{'), '}'));
719 if (preg_match("/^{LABEL_DYNFIELD_([0-9]+)_$uform_name}$/", $pattern, $match)) {
720 /** dynamic field label */
721 $field_id = $match[1];
722 $value = $dynamic_fields[$field_id]->getName();
724 if (preg_match("/^{(INPUT_|VALUE_)?DYNFIELD_([0-9]+)_$uform_name}$/", $pattern, $match)) {
725 /** dynamic field value */
726 $capacity = trim($match[1], '_');
727 $field_id = $match[2];
728 $field_name = $dynamic_fields[$field_id]->getName();
729 $field_type = $dynamic_fields[$field_id]->getType();
731 if ($object !== null) {
732 $all_values = $object->getDynamicFields()->getValues($field_id);
733 foreach ($all_values as $field_value) {
734 $field_values[$field_value['field_val']] = $field_value['text_val'] ??
$field_value['field_val'];
740 switch ($field_type) {
741 case DynamicField
::CHOICE
:
742 $choice_values = $dynamic_fields[$field_id]->getValues();
743 if ($capacity == 'INPUT') {
744 foreach ($choice_values as $choice_idx => $choice_value) {
745 $value .= '<input type="radio" class="box" name="' . $field_name . '" value="' . $field_id . '"';
746 if (isset($field_values[$choice_idx])) {
747 $value .= ' checked="checked"';
749 $value .= ' disabled="disabled">' . $choice_value . ' ';
752 foreach ($field_values as $field_value) {
753 $value .= $field_value;
757 case DynamicField
::BOOLEAN
:
758 foreach ($field_values as $field_value) {
759 $value .= ($field_value ?
_T("Yes") : _T("No"));
762 case DynamicField
::FILE
:
764 foreach ($field_values as $field_value) {
765 if (empty($field_value)) {
768 $spattern = (($this instanceof Texts
) ?
770 '<a href="%1$s%2$s">%3$s</a>'
774 $this->preferences
->getURL(),
775 $this->routeparser
->urlFor(
778 'form_name' => $form_name,
782 'name' => $field_value
789 case DynamicField
::TEXT
:
790 case DynamicField
::LINE
:
791 case DynamicField
::DATE
:
792 $value .= implode('<br/>', $field_values);
797 $this->setReplacements(array($key => $value));
798 Analog
::log("adding dynamic replacement $key => $value", Analog
::DEBUG
);
809 public function getLegend(): array
814 'title' => _T('Main information'),
815 'patterns' => $this->getMainPatterns()
818 $legend['member'] = [
819 'title' => _T('Member information'),
820 'patterns' => $this->getMemberPatterns(false)
827 * Get configured replacements
831 public function getReplacements(): array
833 return $this->replaces
;
839 * @param Db $db Db instance
843 public function setDb(Db
$db): self
850 * Set Login dependency
852 * @param Login $login Login instance
856 public function setLogin(Login
$login): self
858 $this->login
= $login;
863 * Set Preferences dependency
865 * @param Preferences $preferences Preferences instance
869 public function setPreferences(Preferences
$preferences): self
871 $this->preferences
= $preferences;
876 * Set RouteParser dependency
878 * @param RouteParser $routeparser RouteParser instance
882 public function setRouteparser(RouteParser
$routeparser): self
884 $this->routeparser
= $routeparser;
889 * Proceed replacement on given entry
891 * @param string $source Source string
895 protected function proceedReplacements(string $source): string
897 //handle translations
898 $callback = static function ($matches) {
899 return _T($matches[1]);
901 $replaced = preg_replace_callback(
902 '/_T\("([^\"]+)"\)/',
908 ksort($this->patterns
, SORT_NATURAL
);
909 ksort($this->replaces
, SORT_NATURAL
);
911 if (array_keys($this->patterns
) !== array_keys($this->replaces
)) {
912 throw new \
RuntimeException('Patterns and replacements does not match!');
915 //handle replacements
916 $replaced = preg_replace(
922 //handle translations with replacements
923 $repl_callback = static function ($matches) {
930 $replaced = preg_replace_callback(
931 '/str_replace\(\'([^,]+)\', ?\'([^,]+)\', ?\'(.*)\'\)/',
936 return trim($replaced);
944 public function getPatterns(): array
946 return $this->patterns
;