1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2023-2026
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Util\UI;
24:
25: use \Base;
26: use \Markdown;
27: use \Template as F3Template;
28: use SimpleID\Util\ArrayWrapper;
29:
30: /**
31: * An extension to the Fat-Free Framework `Template` class.
32: *
33: * This class implements a numbner of extensions:
34: *
35: * - **Attachments.** This class uses the {@link AttachmentManagerTrait}
36: * to provide methods to allow templates to retrieve attachments.
37: * - **Return values.** The `return` custom tag, along with the {@getReturnValue()}
38: * function, allows templates to assign and return values back to the
39: * calling PHP code.
40: * - **Custom tags.** This class provides custom tags such as `capture`,
41: * `mail_callout` and `mail_button`.
42: * - **Custom filters.**. This class provides a number of additional custom
43: * filters, including `attr`, `js`, `markdown` and `html_to_text`.
44: *
45: */
46: class Template extends F3Template implements AttachmentManagerInterface {
47: use AttachmentManagerTrait;
48:
49: /** @var ArrayWrapper */
50: protected $returnValues;
51:
52: public function __construct() {
53: // tags are automatically registered here
54: parent::__construct();
55:
56: $this->returnValues = new ArrayWrapper();
57:
58: // $this->attachments comes from AttachmentManagerTrait
59: // $this->fw comes from the Fat-Free View class
60: if (!$this->fw->exists('attachments')) $this->fw->set('attachments', []);
61: $this->attachments = &$this->fw->ref('attachments');
62:
63: // Register filters
64: $this->filter('attr', static::class . '::instance()->attr');
65: $this->filter('js', static::class . '::instance()->js');
66: $this->filter('markdown', static::class . '::instance()->markdown');
67: $this->filter('html_to_text', static::class . '::instance()->html_to_text');
68: }
69:
70: /**
71: * Captures the output.
72: *
73: * ```
74: * <capture to="varname"><include href="foo"></capture>
75: * ```
76: *
77: * @param array<string, mixed> $node the template node
78: * @return string the compiled PHP code
79: */
80: public function _capture(array $node) {
81: $attrib = $node['@attrib'];
82: unset($node['@attrib']);
83:
84: if ($attrib['to'] && preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $attrib['to'])) {
85: return '<?php ob_start(); ?>'
86: . $this->build($node)
87: . '<?php $' . $attrib['to'] .' = ob_get_clean(); ?>';
88: } else {
89: return '';
90: }
91: }
92:
93: /**
94: * Sets a return value.
95: *
96: * ```
97: * <return name="value">
98: * ```
99: *
100: * @param array<string, mixed> $node the template node
101: * @return string the compiled PHP code
102: */
103: public function _return(array $node) {
104: $out = '';
105: foreach ($node['@attrib'] as $key => $val) {
106: $out .= '$this->setReturnValue(' . Base::instance()->stringify($key) . ', '.
107: (preg_match('/\{\{(.+?)\}\}/',$val?:'')?
108: $this->token($val):
109: Base::instance()->stringify($val)).'); ';
110: }
111: return '<?php '.$out.'?>';
112: }
113:
114: /**
115: * Format a callout for use in emails
116: *
117: * @param array<string, mixed> $node the template node
118: * @return string the compiled PHP code
119: */
120: public function _mail_callout(array $node) {
121: $callout_bg = '<?= (' . $this->token('@@lightmode.callout_bg') . ') ?>';
122: $callout_text = '<?= (' . $this->token('@@lightmode.callout_text') . ') ?>';
123:
124: $out = '<div style="padding: 0 20px 20px;">';
125: $out .= '<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">';
126: $out .= '<tr>';
127: $out .= '<td class="callout-td" style="background:' . $callout_bg . ';">';
128: $out .= '<div class="callout-div" style="background: ' . $callout_bg . '; font-family: sans-serif; font-size: 21px; line-height: 21px; text-decoration: none; padding: 19px 23px; color: ' . $callout_text . ';">';
129: $out .= $this->build($node);
130: $out .= '</div></td>';
131: $out .= '</tr>';
132: $out .= '</table>';
133: $out .= '</div>';
134:
135: return $out;
136: }
137:
138: /**
139: * Format a button for use in emails
140: *
141: * ```
142: * <mail_button href="value">
143: * ```
144: *
145: * @param array<string, mixed> $node the template node
146: * @return string the compiled PHP code
147: */
148: public function _mail_button(array $node) {
149: $attrib = $node['@attrib'];
150: unset($node['@attrib']);
151:
152: $button_bg = '<?= (' . $this->token('@@lightmode.button_bg') . ') ?>';
153: $button_border = '<?= (' . $this->token('@@lightmode.button_border') . ') ?>';
154: $button_text = '<?= (' . $this->token('@@lightmode.button_text') . ') ?>';
155: $href = (preg_match('/\{\{(.+?)\}\}/',$attrib['href']?:'')?
156: $this->token($attrib['href']):
157: Base::instance()->stringify($attrib['href']));
158:
159: $out = '<div style="padding: 0 20px 20px;">';
160: $out .= '<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">';
161: $out .= '<tr>';
162: $out .= '<td class="button-td button-td-primary" style="border-radius: 4px; background:' . $button_bg . ';">';
163: $out .= '<a class="button-a button-a-primary" href="' . $href . '" style="background: ' . $button_bg . '; border: 1px solid ' . $button_border . '; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: ' . $button_text . '; display: block; border-radius: 4px;">';
164: $out .= $this->build($node);
165: $out .= '</a></td>';
166: $out .= '</tr>';
167: $out .= '</table>';
168: $out .= '</div>';
169:
170: return $out;
171: }
172:
173: /**
174: * Sets a value in the return values array.
175: *
176: * @param string $path the ArrayWrapper path to the value to
177: * set
178: * @param mixed $val the value to set
179: * @return void
180: */
181: protected function setReturnValue(string $path, $val) {
182: $this->returnValues->set($path, $val);
183: }
184:
185: /**
186: * Returns all return values
187: *
188: * @return array<string, mixed> an array of return values
189: */
190: public function getReturnValues(): array {
191: return $this->returnValues->toArray();
192: }
193:
194: /**
195: * Returns a value from the return values.
196: *
197: * @param string $path the ArrayWrapper path to the value to
198: * return
199: * @return mixed
200: */
201: public function getReturnValue(string $path) {
202: return $this->returnValues->get($path);
203: }
204:
205: /**
206: * Filter to create an HTML attribute.
207: *
208: * The output should be fed through a `raw` filter to prevent double-escaping.
209: *
210: * @param mixed $val the attribute value
211: * @param string $name the name of the attribute
212: * @return string
213: */
214: public function attr(mixed $val = null, string $name = null): string {
215: if (($val == null) || ($val == false)) return '';
216: if ($val === true) return $this->esc($name);
217: return $this->esc($name) . '="' . $this->esc($val) . '"';
218: }
219:
220: /**
221: * Filter to encode values to be included in Javascript.
222: *
223: * This function uses `json_encode()` to encode the data as JSON. However,
224: * it provides additional safety features so that they can be embedded
225: * directly within `<script>` tags, including:
226: *
227: * - if the input data is a single string, convert the double quotes to
228: * single quotes
229: * - wrapping arrays and objects with `JSON.parse()`
230: *
231: * The output should be fed through a `raw` filter to prevent double-escaping.
232: *
233: * @param mixed $data the data to be converted
234: * @return string
235: */
236: public function js(mixed $data = null): string {
237: /* Note that all strings in $data have been escaped by F3. This
238: means we have to leave ampersands intact so that they can be
239: unescaped by the raw filter
240: */
241: $json_flags = JSON_HEX_TAG | JSON_HEX_APOS /*| JSON_HEX_AMP*/ | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
242: $json = json_encode($data, $json_flags);
243:
244: if (is_null($data) || is_numeric($data) || is_bool($data)) {
245: // Simple types - return directly
246: return $json;
247: } elseif (is_string($data)) {
248: // String - change quotation marks
249: return "'" . substr($json, 1, -1) . "'";
250: } elseif (($json == '[]') || ($json == '{}')) {
251: // Empty object or array, return directly
252: return $json;
253: } else {
254: // Complex type, wrap JSON parse
255: $json = json_encode($json, $json_flags);
256: return 'JSON.parse(\'' . substr($json, 1, -1) . '\')';
257: }
258: }
259:
260: /**
261: * Converts Markdown to HTML.
262: *
263: * @param string $md Markdown string to convert
264: * @return string the HTML converted from Markdown
265: */
266: public function markdown(string $md): string {
267: $parsedown = new \Parsedown();
268: return $parsedown->text($md);
269: }
270:
271: /**
272: * Transforms HTML to text by stripping HTML tags and word wrapping.
273: *
274: * The contents within the `<head>`, `<style>` and `<script>` tags are removed
275: * altogether
276: *
277: * @param string $html the HTML text
278: * @param int $wordwrap number of characters for word wrapping
279: * @return string the plain text
280: */
281: public function html_to_text(string $html, int $wordwrap = 70): string {
282: return wordwrap(strip_tags(preg_replace('{<(head|style|script)\b.*?</\1>}is', '', $html)), $wordwrap);
283: }
284: }
285:
286: ?>