1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2023-2025
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 to
32: * support attachments.
33: *
34: */
35: class Template extends F3Template implements AttachmentManagerInterface {
36: use AttachmentManagerTrait;
37:
38: /** @var ArrayWrapper */
39: protected $returnValues;
40:
41: public function __construct() {
42: // tags are automatically registered here
43: parent::__construct();
44:
45: $this->returnValues = new ArrayWrapper();
46:
47: // $this->attachments comes from AttachmentManagerTrait
48: // $this->fw comes from the Fat-Free View class
49: if (!$this->fw->exists('attachments')) $this->fw->set('attachments', []);
50: $this->attachments = &$this->fw->ref('attachments');
51:
52: // Register filters
53: $this->filter('attr', static::class . '::instance()->attr');
54: $this->filter('js', static::class . '::instance()->js');
55: $this->filter('markdown', static::class . '::instance()->markdown');
56: $this->filter('html_to_text', static::class . '::instance()->html_to_text');
57: }
58:
59: /**
60: * Captures the output.
61: *
62: * ```
63: * <capture to="varname"><include href="foo"></capture>
64: * ```
65: *
66: * @param array<string, mixed> $node the template node
67: * @return string the compiled PHP code
68: */
69: public function _capture(array $node) {
70: $attrib = $node['@attrib'];
71: unset($node['@attrib']);
72:
73: if ($attrib['to'] && preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $attrib['to'])) {
74: return '<?php ob_start(); ?>'
75: . $this->build($node)
76: . '<?php $' . $attrib['to'] .' = ob_get_clean(); ?>';
77: } else {
78: return '';
79: }
80: }
81:
82: /**
83: * Sets a return value.
84: *
85: * ```
86: * <return name="value">
87: * ```
88: *
89: * @param array<string, mixed> $node the template node
90: * @return string the compiled PHP code
91: */
92: public function _return(array $node) {
93: $out = '';
94: foreach ($node['@attrib'] as $key => $val) {
95: $out .= '$this->setReturnValue(' . Base::instance()->stringify($key) . ', '.
96: (preg_match('/\{\{(.+?)\}\}/',$val?:'')?
97: $this->token($val):
98: Base::instance()->stringify($val)).'); ';
99: }
100: return '<?php '.$out.'?>';
101: }
102:
103: /**
104: * Format a callout for use in emails
105: *
106: * @param array<string, mixed> $node the template node
107: * @return string the compiled PHP code
108: */
109: public function _mail_callout(array $node) {
110: $callout_bg = '<?= (' . $this->token('@@lightmode.callout_bg') . ') ?>';
111: $callout_text = '<?= (' . $this->token('@@lightmode.callout_text') . ') ?>';
112:
113: $out = '<div style="padding: 0 20px 20px;">';
114: $out .= '<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">';
115: $out .= '<tr>';
116: $out .= '<td class="callout-td" style="background:' . $callout_bg . ';">';
117: $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 . ';">';
118: $out .= $this->build($node);
119: $out .= '</div></td>';
120: $out .= '</tr>';
121: $out .= '</table>';
122: $out .= '</div>';
123:
124: return $out;
125: }
126:
127: /**
128: * Format a button for use in emails
129: *
130: * ```
131: * <mail_button href="value">
132: * ```
133: *
134: * @param array<string, mixed> $node the template node
135: * @return string the compiled PHP code
136: */
137: public function _mail_button(array $node) {
138: $attrib = $node['@attrib'];
139: unset($node['@attrib']);
140:
141: $button_bg = '<?= (' . $this->token('@@lightmode.button_bg') . ') ?>';
142: $button_border = '<?= (' . $this->token('@@lightmode.button_border') . ') ?>';
143: $button_text = '<?= (' . $this->token('@@lightmode.button_text') . ') ?>';
144: $href = (preg_match('/\{\{(.+?)\}\}/',$attrib['href']?:'')?
145: $this->token($attrib['href']):
146: Base::instance()->stringify($attrib['href']));
147:
148: $out = '<div style="padding: 0 20px 20px;">';
149: $out .= '<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">';
150: $out .= '<tr>';
151: $out .= '<td class="button-td button-td-primary" style="border-radius: 4px; background:' . $button_bg . ';">';
152: $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;">';
153: $out .= $this->build($node);
154: $out .= '</a></td>';
155: $out .= '</tr>';
156: $out .= '</table>';
157: $out .= '</div>';
158:
159: return $out;
160: }
161:
162: /**
163: * Sets a value in the return values array.
164: *
165: * @param string $path the ArrayWrapper path to the value to
166: * set
167: * @param mixed $val the value to set
168: * @return void
169: */
170: protected function setReturnValue(string $path, $val) {
171: $this->returnValues->set($path, $val);
172: }
173:
174: /**
175: * Returns all return values
176: *
177: * @return array<string, mixed> an array of return values
178: */
179: public function getReturnValues(): array {
180: return $this->returnValues->toArray();
181: }
182:
183: /**
184: * Returns a value from the return values.
185: *
186: * @param string $path the ArrayWrapper path to the value to
187: * return
188: * @return mixed
189: */
190: public function getReturnValue(string $path) {
191: return $this->returnValues->get($path);
192: }
193:
194: /**
195: * Filter to create an HTML attribute.
196: *
197: * The output should be fed through a `raw` filter to prevent double-escaping.
198: *
199: * @param mixed $val the attribute value
200: * @param string $name the name of the attribute
201: * @return string
202: */
203: public function attr(mixed $val = null, string $name = null): string {
204: if (($val == null) || ($val == false)) return '';
205: if ($val === true) return $this->esc($name);
206: return $this->esc($name) . '="' . $this->esc($val) . '"';
207: }
208:
209: /**
210: * Filter to encode values to be included in Javascript.
211: *
212: * This function uses `json_encode()` to encode the data as JSON. However,
213: * it provides additional safety features so that they can be embedded
214: * directly within `<script>` tags, including:
215: *
216: * - if the input data is a single string, convert the double quotes to
217: * single quotes
218: * - wrapping arrays and objects with `JSON.parse()`
219: *
220: * The output should be fed through a `raw` filter to prevent double-escaping.
221: *
222: * @param mixed $data the data to be converted
223: * @return string
224: */
225: public function js(mixed $data = null): string {
226: /* Note that all strings in $data have been escaped by F3. This
227: means we have to leave ampersands intact so that they can be
228: unescaped by the raw filter
229: */
230: $json_flags = JSON_HEX_TAG | JSON_HEX_APOS /*| JSON_HEX_AMP*/ | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
231: $json = json_encode($data, $json_flags);
232:
233: if (is_null($data) || is_numeric($data) || is_bool($data)) {
234: // Simple types - return directly
235: return $json;
236: } elseif (is_string($data)) {
237: // String - change quotation marks
238: return "'" . substr($json, 1, -1) . "'";
239: } elseif (($json == '[]') || ($json == '{}')) {
240: // Empty object or array, return directly
241: return $json;
242: } else {
243: // Complex type, wrap JSON parse
244: $json = json_encode($json, $json_flags);
245: return 'JSON.parse(\'' . substr($json, 1, -1) . '\')';
246: }
247: }
248:
249: /**
250: * Converts Markdown to HTML.
251: *
252: * @param string $md Markdown string to convert
253: * @return string the HTML converted from Markdown
254: */
255: public function markdown(string $md): string {
256: $parsedown = new \Parsedown();
257: return $parsedown->text($md);
258: }
259:
260: /**
261: * Transforms HTML to text by stripping HTML tags and word wrapping.
262: *
263: * The contents within the `<head>`, `<style>` and `<script>` tags are removed
264: * altogether
265: *
266: * @param string $html the HTML text
267: * @param int $wordwrap number of characters for word wrapping
268: * @return string the plain text
269: */
270: public function html_to_text(string $html, int $wordwrap = 70): string {
271: return wordwrap(strip_tags(preg_replace('{<(head|style|script)\b.*?</\1>}is', '', $html)), $wordwrap);
272: }
273: }
274:
275: ?>