Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.87% |
86 / 99 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
icon | |
86.87% |
86 / 99 |
|
33.33% |
3 / 9 |
44.81 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFunctions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
icon | |
83.67% |
41 / 49 |
|
0.00% |
0 / 1 |
18.26 | |||
insert_fa_class | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
prepare_svg | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
9 | |||
get_first_icon | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
implode_attributes | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
get_style_list | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb\template\twig\extension; |
15 | |
16 | use phpbb\template\twig\environment; |
17 | use Twig\Extension\AbstractExtension; |
18 | |
19 | class icon extends AbstractExtension |
20 | { |
21 | /** @var \phpbb\user */ |
22 | protected $user; |
23 | |
24 | /** |
25 | * Constructor. |
26 | * |
27 | * @param \phpbb\user $user User object |
28 | */ |
29 | public function __construct(\phpbb\user $user) |
30 | { |
31 | $this->user = $user; |
32 | } |
33 | |
34 | /** |
35 | * Returns the name of this extension. |
36 | * |
37 | * @return string The extension name |
38 | */ |
39 | public function getName() |
40 | { |
41 | return 'icon'; |
42 | } |
43 | |
44 | /** |
45 | * Returns a list of functions to add to the existing list. |
46 | * |
47 | * @return \Twig\TwigFunction[] Array of twig functions |
48 | */ |
49 | public function getFunctions() |
50 | { |
51 | return [ |
52 | new \Twig\TwigFunction('Icon', [$this, 'icon'], ['needs_environment' => true]), |
53 | ]; |
54 | } |
55 | |
56 | /** |
57 | * Generate icon HTML for use in the template, depending on the mode. |
58 | * |
59 | * @param environment $environment Twig environment object |
60 | * @param string $type Icon type (font|png|svg) |
61 | * @param array|string $icon Icon name (eg. "bold") |
62 | * @param string $title Icon title |
63 | * @param bool $hidden Hide the icon title from view |
64 | * @param string $classes Additional classes (eg. "fa-fw") |
65 | * @param array $attributes Additional attributes for the icon, where the key is the attribute. |
66 | * {'data-ajax': 'mark_forums'} results in ' data-ajax="mark_forums"' |
67 | * @return string |
68 | */ |
69 | public function icon(environment $environment, $type, $icon, $title = '', $hidden = false, $classes = '', array $attributes = []) |
70 | { |
71 | $type = strtolower($type); |
72 | $icon = is_array($icon) ? $this->get_first_icon($icon) : $icon; |
73 | |
74 | if (empty($icon)) |
75 | { |
76 | return ''; |
77 | } |
78 | |
79 | $not_found = false; |
80 | $source = ''; |
81 | $view_box = ''; |
82 | |
83 | switch ($type) |
84 | { |
85 | case 'font': |
86 | $classes = $this->insert_fa_class($classes); |
87 | break; |
88 | |
89 | case 'png': |
90 | $filesystem = $environment->get_filesystem(); |
91 | $root_path = $environment->get_web_root_path(); |
92 | |
93 | // Iterate over the user's styles and check for icon existance |
94 | foreach ($this->get_style_list() as $style_path) |
95 | { |
96 | if ($filesystem->exists("{$root_path}styles/{$style_path}/theme/png/{$icon}.png")) |
97 | { |
98 | $source = "{$root_path}styles/{$style_path}/theme/png/{$icon}.png"; |
99 | |
100 | break; |
101 | } |
102 | } |
103 | |
104 | // Check if the icon was found or not |
105 | $not_found = empty($source); |
106 | break; |
107 | |
108 | case 'svg': |
109 | try |
110 | { |
111 | // Try to load and prepare the SVG icon |
112 | $file = $environment->load('svg/' . $icon . '.svg'); |
113 | $source = $this->prepare_svg($file, $view_box); |
114 | |
115 | if (empty($view_box)) |
116 | { |
117 | return ''; |
118 | } |
119 | } |
120 | catch (\Twig\Error\LoaderError $e) |
121 | { |
122 | // Icon was not found |
123 | $not_found = true; |
124 | } |
125 | catch (\Twig\Error\Error $e) |
126 | { |
127 | return $e->getMessage(); |
128 | } |
129 | break; |
130 | |
131 | default: |
132 | return ''; |
133 | } |
134 | |
135 | // If no PNG or SVG icon was found, display a default 404 SVG icon. |
136 | if ($not_found) |
137 | { |
138 | try |
139 | { |
140 | $file = $environment->load('svg/404.svg'); |
141 | $source = $this->prepare_svg($file, $view_box); |
142 | } |
143 | catch (\Twig\Error\Error $e) |
144 | { |
145 | return $e->getMessage(); |
146 | } |
147 | |
148 | $type = 'svg'; |
149 | $icon = '404'; |
150 | } |
151 | |
152 | try |
153 | { |
154 | return $environment->render("macros/icons/{$type}.twig", [ |
155 | 'ATTRIBUTES' => (string) $this->implode_attributes($attributes), |
156 | 'CLASSES' => (string) $classes, |
157 | 'ICON' => (string) $icon, |
158 | 'SOURCE' => (string) $source, |
159 | 'TITLE' => (string) $title, |
160 | 'TITLE_ID' => $title && $type === 'svg' ? unique_id() : '', |
161 | 'VIEW_BOX' => (string) $view_box, |
162 | 'S_HIDDEN' => (bool) $hidden, |
163 | ]); |
164 | } |
165 | catch (\Twig\Error\Error $e) |
166 | { |
167 | return $e->getMessage(); |
168 | } |
169 | } |
170 | |
171 | /** |
172 | * Insert fa class into class string by checking if class string contains any fa classes |
173 | * |
174 | * @param string $class_string |
175 | * @return string Updated class string or original class string if fa class is already set or string is empty |
176 | */ |
177 | protected function insert_fa_class(string $class_string): string |
178 | { |
179 | if (empty($class_string)) |
180 | { |
181 | return $class_string; |
182 | } |
183 | |
184 | // These also include pro class name we don't use, but handle them properly anyway |
185 | $fa_classes = ['fa-solid', 'fas', 'fa-regular', 'far', 'fal', 'fa-light', 'fab', 'fa-brands']; |
186 | |
187 | // Split the class string into individual words |
188 | $icon_classes = explode(' ', $class_string); |
189 | |
190 | // Check if the class string contains any of the fa classes, just return class string in that case |
191 | foreach ($icon_classes as $word) |
192 | { |
193 | if (in_array($word, $fa_classes)) |
194 | { |
195 | return $class_string; |
196 | } |
197 | } |
198 | |
199 | // If we reach this it means we didn't have any fa classes in the class string. |
200 | // Prepend class string with fas for fa-solid |
201 | return 'fas ' . $class_string; |
202 | } |
203 | |
204 | /** |
205 | * Prepare an SVG for usage in the template icon. |
206 | * |
207 | * This removes any <?xml ?> and <!DOCTYPE> elements, |
208 | * aswell as the root <svg> and any <title> elements. |
209 | * |
210 | * @param \Twig\TemplateWrapper $file The SVG file loaded from the environment |
211 | * @param string $view_box The viewBox attribute value |
212 | * @return string The cleaned SVG |
213 | */ |
214 | protected function prepare_svg(\Twig\TemplateWrapper $file, &$view_box = '') |
215 | { |
216 | $code = $file->render(); |
217 | $code = preg_replace( "/<\?xml.+?\?>/", '', $code); |
218 | |
219 | $doc = new \DOMDocument(); |
220 | $doc->preserveWhiteSpace = false; |
221 | |
222 | // Hide html5/svg errors |
223 | libxml_use_internal_errors(true); |
224 | |
225 | // Options parameter prevents $dom->saveHTML() from adding an <html> element. |
226 | $doc->loadHTML($code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); |
227 | |
228 | // Remove any DOCTYPE |
229 | foreach ($doc->childNodes as $child) |
230 | { |
231 | if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) |
232 | { |
233 | $child->parentNode->removeChild($child); |
234 | } |
235 | } |
236 | |
237 | $xpath = new \DOMXPath($doc); |
238 | |
239 | /** |
240 | * Remove the root <svg> element |
241 | * and all <title> elements. |
242 | * |
243 | * @var \DOMElement $element |
244 | */ |
245 | foreach ($xpath->query('/svg | //title') as $element) |
246 | { |
247 | if ($element->nodeName === 'svg') |
248 | { |
249 | // Return the viewBox attribute value of the root SVG element by reference |
250 | $view_box = $element->getAttribute('viewbox'); |
251 | |
252 | $width = $element->getAttribute('width'); |
253 | $height = $element->getAttribute('height'); |
254 | |
255 | if (empty($view_box) && $width && $height) |
256 | { |
257 | $view_box = "0 0 {$width} {$height}"; |
258 | } |
259 | |
260 | while (isset($element->firstChild)) |
261 | { |
262 | $element->parentNode->insertBefore($element->firstChild, $element); |
263 | } |
264 | } |
265 | |
266 | $element->parentNode->removeChild($element); |
267 | } |
268 | |
269 | $string = $doc->saveHTML(); |
270 | $string = preg_replace('/\s+/', ' ', $string); |
271 | |
272 | return $string; |
273 | } |
274 | |
275 | /** |
276 | * Finds the first icon that has a "true" value and returns it. |
277 | * |
278 | * This allows sending an array to the Icon() function, |
279 | * where the keys are the icon names and the values are their checks. |
280 | * |
281 | * {{ Icon('font', { |
282 | * 'bullhorn': topicrow.S_POST_GLOBAL or topicrow.S_POST_ANNOUNCE, |
283 | * 'thumbtack': topicrow.S_POST_STICKY, |
284 | * 'lock': topicrow.S_TOPIC_LOCKED, |
285 | * 'fire': topicrow.S_TOPIC_HOT, |
286 | * 'file': true, |
287 | * }, lang('MY_TITLE'), true) }} |
288 | * |
289 | * @param array $icons Array of icons and their booleans |
290 | * @return string The first 'true' icon |
291 | */ |
292 | protected function get_first_icon(array $icons) |
293 | { |
294 | foreach ($icons as $icon => $boolean) |
295 | { |
296 | // In case the key is not a string, |
297 | // this icon does not have a check |
298 | // so instantly return it |
299 | if (!is_string($icon)) |
300 | { |
301 | return $boolean; |
302 | } |
303 | |
304 | if ($boolean) |
305 | { |
306 | return $icon; |
307 | } |
308 | } |
309 | |
310 | return ''; |
311 | } |
312 | |
313 | /** |
314 | * Implode an associated array of attributes to a string for usage in a template. |
315 | * |
316 | * @param array $attributes Associated array of attributes |
317 | * @return string |
318 | */ |
319 | protected function implode_attributes(array $attributes) |
320 | { |
321 | $string = ''; |
322 | |
323 | foreach ($attributes as $key => $value) |
324 | { |
325 | $string .= ' ' . $key . '="' . $value . '"'; |
326 | } |
327 | |
328 | return $string; |
329 | } |
330 | |
331 | /** |
332 | * Get the style tree of the style preferred by the current user. |
333 | * |
334 | * @return array Style tree, most specific first |
335 | */ |
336 | protected function get_style_list() |
337 | { |
338 | $style_list = [$this->user->style['style_path']]; |
339 | |
340 | if ($this->user->style['style_parent_id']) |
341 | { |
342 | $style_list = array_merge($style_list, array_reverse(explode('/', $this->user->style['style_parent_tree']))); |
343 | } |
344 | |
345 | return $style_list; |
346 | } |
347 | } |