]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Texts.php
Fix missing texts insertion; closes #1587
[galette.git] / galette / lib / Galette / Entity / Texts.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Texts 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 Entity
28 * @package Galette
29 *
30 * @author John Perr <johnperr@abul.org>
31 * @author Johan Cwiklinski <joahn@x-tnd.be>
32 * @copyright 2007-2021 The Galette Team
33 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
34 * @link http://galette.tuxfamily.org
35 * @since Avaialble since 0.7dev - 2007-07-16
36 */
37
38 namespace Galette\Entity;
39
40 use Galette\Core\I18n;
41 use Galette\Features\Replacements;
42 use Throwable;
43 use Analog\Analog;
44 use Laminas\Db\Sql\Expression;
45 use Galette\Core\Password;
46 use Galette\Core\Preferences;
47 use Slim\Router;
48
49 /**
50 * Texts class for galette
51 *
52 * @category Entity
53 * @name Texts
54 * @package Galette
55 * @author John Perr <johnperr@abul.org>
56 * @author Johan Cwiklinski <joahn@x-tnd.be>
57 * @copyright 2007-2021 The Galette Team
58 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
59 * @link http://galette.tuxfamily.org
60 * @since Avaialble since 0.7dev - 2007-07-16
61 */
62 class Texts
63 {
64 use Replacements {
65 getLegend as protected trait_getLegend;
66 }
67
68 private $all_texts;
69 public const TABLE = "texts";
70 public const PK = 'tid';
71 public const DEFAULT_REF = 'sub';
72
73 private $defaults;
74 private $current;
75
76 /**
77 * Main constructor
78 *
79 * @param Preferences $preferences Galette's preferences
80 * @param Router|null $router Router instance
81 */
82 public function __construct(Preferences $preferences, Router $router = null)
83 {
84 global $zdb, $login, $container;
85 $this->preferences = $preferences;
86 if ($router === null) {
87 $router = $container->get('router');
88 }
89 if ($login === null) {
90 $login = $container->get('login');
91 }
92 $this->router = $router;
93 $this
94 ->setDb($zdb)
95 ->setLogin($login);
96
97 $this->setPatterns(
98 $this->getMainPatterns()
99 + $this->getMailPatterns()
100 + $this->getMemberPatterns()
101 + $this->getContributionPatterns()
102 );
103
104 if (GALETTE_MODE !== 'INSTALL') {
105 $this
106 ->setMain()
107 ->setMail();
108 }
109 }
110
111 /**
112 * Get patterns for mails
113 *
114 * @param boolean $legacy Whether to load legacy patterns
115 *
116 * @return array
117 */
118 protected function getMailPatterns($legacy = true): array
119 {
120 $m_patterns = [
121 'breakline' => [
122 'title' => _T('Insert a carriage return'),
123 'pattern' => '/{BR}/',
124 ],
125 'newline' => [
126 'title' => _T('Insert a new blank line'),
127 'pattern' => '/{NEWLINE}/',
128 ],
129 'link_validity' => [
130 'title' => _T('Link validity'),
131 'pattern' => '/{LINK_VALIDITY}/',
132 'onlyfor' => ['sub', 'pwd']
133 ],
134 'link_membercard' => [
135 'title' => _T('Direct link for member card download'),
136 'pattern' => '/{LINK_MEMBERCARD}/',
137 'onlyfor' => ['contrib', 'donation']
138 ],
139 'link_contribpdf' => [
140 'title' => _T('Direct link for invoice/receipt download'),
141 'pattern' => '/{LINK_CONTRIBPDF}/',
142 'onlyfor' => ['contrib', 'donation']
143 ],
144 'change_pass_uri' => [
145 'title' => _T("Galette's change password URI"),
146 'pattern' => '/{CHG_PWD_URI}/',
147 'onlyfor' => ['sub', 'pwd']
148 ],
149 ];
150
151 //clean based on current ref and onlyfor
152 if ($this->current !== null) {
153 foreach ($m_patterns as $key => $m_pattern) {
154 if (
155 isset($m_pattern['onlyfor'])
156 && !in_array($this->current, $m_pattern['onlyfor'])
157 ) {
158 unset($m_patterns[$key]);
159 }
160 }
161 }
162
163 return $m_patterns;
164 }
165
166 /**
167 * Set emails replacements
168 *
169 * @return $this
170 */
171 public function setMail(): self
172 {
173 $this->setReplacements([
174 'link_validity' => null,
175 'breakline' => "\r\n",
176 'newline' => "\r\n\r\n",
177 'link_membercard' => null,
178 'link_contribpdf' => null,
179 'change_pass_uri' => null
180 ]);
181 return $this;
182 }
183
184 /**
185 * Set change password URL
186 *
187 * @param Password $password Password instance
188 *
189 * @return Texts
190 */
191 public function setChangePasswordURI(Password $password): Texts
192 {
193 $this->setReplacements([
194 'change_pass_uri' => $this->preferences->getURL() .
195 $this->router->pathFor(
196 'password-recovery',
197 ['hash' => base64_encode($password->getHash())]
198 )
199 ]);
200 return $this;
201 }
202
203 /**
204 * Set validity link
205 *
206 * @return Texts
207 */
208 public function setLinkValidity(): Texts
209 {
210 $link_validity = new \DateTime();
211 $link_validity->add(new \DateInterval('PT24H'));
212 $this->setReplacements(['link_validity' => $link_validity->format(_T("Y-m-d H:i:s"))]);
213 return $this;
214 }
215
216 /**
217 * Set member card PDF link
218 *
219 * @param string $link Link
220 *
221 * @return Texts
222 */
223 public function setMemberCardLink(string $link): Texts
224 {
225 $this->setReplacements(['link_membercard' => $link]);
226 return $this;
227 }
228
229 /**
230 * Set contribution PDF link
231 *
232 * @param string $link Link
233 *
234 * @return Texts
235 */
236 public function setContribLink(string $link): Texts
237 {
238 $this->setReplacements(['link_contribpdf' => $link]);
239 return $this;
240 }
241
242 /**
243 * Get specific text
244 *
245 * @param string $ref Reference of text to get
246 * @param string $lang Language texts to get
247 *
248 * @return array of all text fields for one language.
249 */
250 public function getTexts($ref, $lang)
251 {
252 global $i18n;
253
254 //check if language is set and exists
255 $langs = $i18n->getList();
256 $is_lang_ok = false;
257 foreach ($langs as $l) {
258 if ($lang === $l->getID()) {
259 $is_lang_ok = true;
260 break;
261 }
262 }
263
264 if ($is_lang_ok !== true) {
265 Analog::log(
266 'Language ' . $lang .
267 ' does not exists. Falling back to default Galette lang.',
268 Analog::ERROR
269 );
270 $lang = $i18n->getID();
271 }
272
273 try {
274 $select = $this->zdb->select(self::TABLE);
275 $select->where(
276 array(
277 'tref' => $ref,
278 'tlang' => $lang
279 )
280 );
281 $results = $this->zdb->execute($select);
282 $result = $results->current();
283 if ($result) {
284 $this->all_texts = $result;
285 } else {
286 //hum... no result... That means text do not exist in the
287 //database, let's add it
288 $default = null;
289 $this->defaults = $this->getAllDefaults(); //load defaults
290 foreach ($this->defaults as $d) {
291 if ($d['tref'] == $ref && $d['tlang'] == $lang) {
292 $default = $d;
293 break;
294 }
295 }
296 if ($default !== null) {
297 $values = array(
298 'tref' => $default['tref'],
299 'tsubject' => $default['tsubject'],
300 'tbody' => $default['tbody'],
301 'tlang' => $default['tlang'],
302 'tcomment' => $default['tcomment']
303 );
304
305 try {
306 $this->insert([$values]);
307 return $this->getTexts($ref, $lang);
308 } catch (Throwable $e) {
309 Analog::log(
310 'Unable to add missing requested text "' . $ref .
311 ' (' . $lang . ') | ' . $e->getMessage(),
312 Analog::WARNING
313 );
314 }
315 } else {
316 Analog::log(
317 'Unable to find missing requested text "' . $ref .
318 ' (' . $lang . ')',
319 Analog::WARNING
320 );
321 }
322 }
323 return $this->all_texts;
324 } catch (Throwable $e) {
325 Analog::log(
326 'Cannot get text `' . $ref . '` for lang `' . $lang . '` | ' .
327 $e->getMessage(),
328 Analog::WARNING
329 );
330 return false;
331 }
332 }
333
334 /**
335 * Set text
336 *
337 * @param string $ref Texte ref to locate
338 * @param string $lang Texte language to locate
339 * @param string $subject Subject to set
340 * @param string $body Body text to set
341 *
342 * @return integer|false affected rows (0 if record did not change)
343 * or false on error
344 */
345 public function setTexts($ref, $lang, $subject, $body)
346 {
347 try {
348 $values = array(
349 'tsubject' => $subject,
350 'tbody' => $body,
351 );
352
353 $update = $this->zdb->update(self::TABLE);
354 $update->set($values)->where(
355 array(
356 'tref' => $ref,
357 'tlang' => $lang
358 )
359 );
360 $this->zdb->execute($update);
361
362 return true;
363 } catch (Throwable $e) {
364 Analog::log(
365 'An error has occurred while storing email text. | ' .
366 $e->getMessage(),
367 Analog::ERROR
368 );
369 return false;
370 }
371 }
372
373 /**
374 * Ref List
375 *
376 * @param string $lang Requested language
377 *
378 * @return array: list of references used for texts
379 */
380 public function getRefs(string $lang = I18n::DEFAULT_LANG)
381 {
382 try {
383 $select = $this->zdb->select(self::TABLE);
384 $select->columns(
385 array('tref', 'tcomment')
386 )->where(array('tlang' => $lang));
387
388 $refs = [];
389 $results = $this->zdb->execute($select);
390 foreach ($results as $result) {
391 $refs[] = $result;
392 }
393 return $refs;
394 } catch (Throwable $e) {
395 Analog::log(
396 'Cannot get refs for lang `' . $lang . '` | ' .
397 $e->getMessage(),
398 Analog::WARNING
399 );
400 return false;
401 }
402 }
403
404 /**
405 * Initialize texts at install time
406 *
407 * @param boolean $check_first Check first if it seem initialized
408 *
409 * @return boolean|Exception false if no need to initialize, true if data
410 * has been initialized, Exception if error
411 */
412 public function installInit($check_first = true)
413 {
414 try {
415 //first of all, let's check if data seem to have already
416 //been initialized
417 $this->defaults = $this->getAllDefaults(); //load defaults
418 $proceed = false;
419 if ($check_first === true) {
420 $select = $this->zdb->select(self::TABLE);
421 $select->columns(
422 array(
423 'counter' => new Expression('COUNT(' . self::PK . ')')
424 )
425 );
426
427 $results = $this->zdb->execute($select);
428 $result = $results->current();
429 $count = $result->counter;
430 if ($count == 0) {
431 //if we got no values in texts table, let's proceed
432 $proceed = true;
433 } else {
434 if ($count < count($this->defaults)) {
435 return $this->checkUpdate();
436 }
437 return false;
438 }
439 } else {
440 $proceed = true;
441 }
442
443 if ($proceed === true) {
444 //first, we drop all values
445 $delete = $this->zdb->delete(self::TABLE);
446 $this->zdb->execute($delete);
447
448 $this->zdb->handleSequence(
449 self::TABLE,
450 count($this->defaults)
451 );
452
453 $this->insert($this->defaults);
454
455 Analog::log(
456 'Default texts were successfully stored into database.',
457 Analog::INFO
458 );
459 return true;
460 }
461 } catch (Throwable $e) {
462 Analog::log(
463 'Unable to initialize default texts.' . $e->getMessage(),
464 Analog::WARNING
465 );
466 throw $e;
467 }
468 }
469
470 /**
471 * Checks for missing texts in the database
472 *
473 * @return boolean
474 */
475 private function checkUpdate()
476 {
477 try {
478 $select = $this->zdb->select(self::TABLE);
479 $dblist = $this->zdb->execute($select);
480
481 $list = [];
482 foreach ($dblist as $dbentry) {
483 $list[] = $dbentry;
484 }
485
486 $missing = array();
487 foreach ($this->defaults as $default) {
488 $exists = false;
489 foreach ($list as $text) {
490 if (
491 $text->tref == $default['tref']
492 && $text->tlang == $default['tlang']
493 ) {
494 $exists = true;
495 continue;
496 }
497 }
498
499 if ($exists === false) {
500 //text does not exists in database, insert it.
501 $missing[] = $default;
502 }
503 }
504
505 if (count($missing) > 0) {
506 $this->insert($missing);
507
508 Analog::log(
509 'Missing texts were successfully stored into database.',
510 Analog::INFO
511 );
512 return true;
513 }
514 } catch (Throwable $e) {
515 Analog::log(
516 'An error occurred checking missing texts.' . $e->getMessage(),
517 Analog::WARNING
518 );
519 throw $e;
520 }
521 }
522
523 /**
524 * Get the subject, with all replacements done
525 *
526 * @return string
527 */
528 public function getSubject()
529 {
530 return $this->proceedReplacements($this->all_texts->tsubject);
531 }
532
533 /**
534 * Get the body, with all replacements done
535 *
536 * @return string
537 */
538 public function getBody()
539 {
540 return $this->proceedReplacements($this->all_texts->tbody);
541 }
542
543 /**
544 * Insert values in database
545 *
546 * @param array $values Values to insert
547 *
548 * @return void
549 */
550 private function insert(array $values)
551 {
552 $insert = $this->zdb->insert(self::TABLE);
553 $insert->values(
554 array(
555 'tref' => ':tref',
556 'tsubject' => ':tsubject',
557 'tbody' => ':tbody',
558 'tlang' => ':tlang',
559 'tcomment' => ':tcomment'
560 )
561 );
562 $stmt = $this->zdb->sql->prepareStatementForSqlObject($insert);
563
564 foreach ($values as $value) {
565 $stmt->execute($value);
566 }
567 }
568
569 /**
570 * Get default mail texts for all languages
571 *
572 * @return array
573 */
574 public function getAllDefaults()
575 {
576 global $i18n;
577
578 $all = [];
579 foreach (array_keys($i18n->getArrayList()) as $lang) {
580 $all = array_merge($all, $this->getDefaultTexts($lang));
581 }
582
583 return $all;
584 }
585
586 /**
587 * Get default texts for specified language
588 *
589 * @param string $lang Requested lang. Defaults to en_US
590 *
591 * @return array
592 */
593 public function getDefaultTexts($lang = 'en_US')
594 {
595 global $i18n;
596
597 $current_lang = $i18n->getID();
598
599 $i18n->changeLanguage($lang);
600
601 //do the magic!
602 include GALETTE_ROOT . 'includes/fields_defs/texts_fields.php';
603 $texts = [];
604
605 foreach ($texts_fields as $text_field) {
606 unset($text_field['tid']);
607 $text_field['tlang'] = $lang;
608 $texts[] = $text_field;
609 }
610
611 //reset to current lang
612 $i18n->changeLanguage($current_lang);
613 return $texts;
614 }
615
616 /**
617 * Build legend array
618 *
619 * @return array
620 */
621 public function getLegend(): array
622 {
623 $legend = $this->trait_getLegend();
624
625 $contribs = ['contrib', 'newcont', 'donation', 'newdonation'];
626 if ($this->current !== null && in_array($this->current, $contribs)) {
627 $patterns = $this->getContributionPatterns(false);
628 $legend['contribution'] = [
629 'title' => _T('Contribution information'),
630 'patterns' => $patterns
631 ];
632 }
633
634 $patterns = $this->getMailPatterns(false);
635 $legend['mail'] = [
636 'title' => _T('Mail specific'),
637 'patterns' => $patterns
638 ];
639
640 return $legend;
641 }
642
643 /**
644 * Set current text reference
645 *
646 * @param string $ref Reference
647 *
648 * @return Texts
649 */
650 public function setCurrent(string $ref): self
651 {
652 $this->current = $ref;
653 return $this;
654 }
655 }