Passed
Push — develop ( 319bd8...330c7a )
by Neill
16:41 queued 15s
created

Html   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Test Coverage

Coverage 38.76%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 121
c 4
b 0
f 0
dl 0
loc 327
ccs 50
cts 129
cp 0.3876
rs 9.44
wmc 37

15 Methods

Rating   Name   Duplication   Size   Complexity  
A displayAsTags() 0 7 2
A buttonEdit() 0 5 1
B highlight() 0 50 8
A buttonAdd() 0 5 1
A purify() 0 22 5
A buttonDelete() 0 5 1
A buttonCustom() 0 5 1
A checkbox() 0 4 1
A gravatar() 0 9 2
A buttonCancel() 0 5 1
A radio() 0 4 1
A favicon() 0 19 2
A processTags() 0 11 1
B sanitise() 0 24 7
A encodeEntities() 0 9 3
1
<?php
2
3
namespace neon\core\helpers;
4
5
use neon\core\helpers\Url;
6
use neon\core\helpers\HtmlPurifier;
7
use DOMDocument;
8
use DOMXPath;
9
use Exception;
10
11
class Html extends \yii\helpers\Html
12
{
13
	/**
14
	 * Show an array of items as tags - using span element and apply specified css class
15
	 *
16
	 * @param array $tagsArray
17
	 * @param string $class - css classes to apply to the wrapping tag
18
	 */
19
	public static function displayAsTags($tagsArray, $class = 'label label-info')
20
	{
21
		$out = '';
22
		foreach($tagsArray as $tag) {
23
			$out .= "<span class=\"$class\">$tag</span>";
24
		}
25
		return $out;
26
	}
27
28
	/**
29
	 * Generate a favicon
30
	 * @param string|array $favicon - a string of a firefly image id 
31
	 *   or an array containing the firefly image id inside 'id' property  
32
	 */
33
	public static function favicon($favicon)
34
	{
35
		$favicon = is_array($favicon) ? Arr::getRequired($favicon, 'id') : $favicon;
36
		
37
		// currently firefly cannot generate ico formats 
38
		// <link rel="shortcut icon" type="image/x-icon" href="{asset path="/images/fav/$fav/favicon.ico"}"> -->
39
		$url = url('firefly/file/img');
40
		return <<<HTML
41
			<link rel="icon" type="image/png" sizes="192x192" href="$url?id=$favicon&w=192&h=192">
42
			<link rel="icon" type="image/png" sizes="32x32" href="$url?id=$favicon&w=32&h=32">
43
			<link rel="icon" type="image/png" sizes="96x96" href="$url?id=$favicon&w=96&h=96">
44
			<link rel="icon" type="image/png" sizes="16x16" href="$url?id=$favicon&w=16&h=16">
45
			<link rel="apple-touch-icon" sizes="57x57" href="$url?id=$favicon&w=57&h=57">
46
			<link rel="apple-touch-icon" sizes="60x60" href="$url?id=$favicon&w=60&h=60">
47
			<link rel="apple-touch-icon" sizes="76x76" href="$url?id=$favicon&w=76&h=76">
48
			<link rel="apple-touch-icon" sizes="120x120" href="$url?id=$favicon&w=120&h=120">
49
			<link rel="apple-touch-icon" sizes="144x144" href="$url?id=$favicon&w=144&h=144">
50
			<link rel="apple-touch-icon" sizes="152x152" href="$url?id=$favicon&w=152&h=152">
51
			<meta name="msapplication-TileImage" content="$url?id=$favicon&w=144&h=144">
52
		HTML;
53
	}
54
55
	/**
56
	 * Return the Html for a custom button
57
	 *
58
	 * @param mixed $url  the URL the button should return to @see \yii\helpers\Url::to
59
	 * @param string $text  the button text
60
	 * @param string $icon  the fa icon to use (can omit the fa-)
61
	 * @param string $class  the class(es) to apply post class btn
62
	 * @param array $options  any additional options
63
	 *  - specify $options['icon']
64
	 * @return string Html for the custom button
65
	 */
66
	public static function buttonCustom($url, $text, array $options=[])
67
	{
68
		$options['class'] = 'btn '. Arr::get($options, 'class', '');
69
		$icon = Arr::remove($options, 'icon', '');
70
		return self::a("<i class=\"$icon\"></i> ".$text, $url, $options);
71
	}
72
73
	/**
74
	 * Return the Html for a standard edit button
75
	 *
76
	 * @param array|string $url @see \yii\helpers\Url::to
77
	 * @param string $text - button text
78
	 * @return string Html for an edit button
79
	 */
80
	public static function buttonEdit($url, $text='Edit', $options=[])
81
	{
82
		$options['icon'] = 'fa fa-pencil';
83
		$options['class'] = 'btn-default ' . Arr::get($options, 'class', '');
84
		return self::buttonCustom($url, $text, $options);
85
	}
86
87
	/**
88
	 * Return the HTML for a standard delete button
89
	 * Note delete often means soft delete (destroy means remove from the db entirely)
90
	 *
91
	 * @param mixed $url
92
	 * @see \yii\helpers\Url::to
93
	 * @return string Html
94
	 */
95
	public static function buttonDelete($url, $text='Delete', $options=[])
96
	{
97
		$options['icon'] = 'fa fa-trash';
98
		$options['class'] =  'btn-danger ' . Arr::get($options, 'class', '');
99
		return self::buttonCustom($url, $text, $options);
100
	}
101
102
	/**
103
	 * Return the HTML for a standard Add button
104
	 *
105
	 * @param mixed $url
106
	 * @param string $text button text
107
	 * @see \yii\helpers\Url::to
108
	 * @return string Html
109
	 */
110
	public static function buttonAdd($url, $text='Add', $options=[])
111
	{
112
		$options['icon'] = 'fa fa-plus';
113
		$options['class'] = 'btn-primary btn-add';
114
		return self::buttonCustom($url, $text, $options);
115
	}
116
117
	/**
118
	 * Return the HTML for a standard cancel button
119
	 *
120
	 * @param mixed $url
121
	 * @param string $text button text
122
	 * @return string Html
123
	 */
124
	public static function buttonCancel($url='', $text='Cancel', $options=[])
125
	{
126
		$options['icon'] = 'fa fa-arrow-left';
127
		$options['class'] = 'btn-default btn-cancel';
128
		return self::buttonCustom(Url::goBack($url), $text, $options);
129
	}
130
131
	/**
132
	 * Get either a Gravatar URL or complete image tag for a specified email address.
133
	 *
134
	 * @param string $email The email address
135
	 * @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ]
136
	 * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
137
	 * @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
138
	 * @param bool $img True to return a complete IMG tag False for just the URL
139
	 * @param array $atts Optional, additional key/value attributes to include in the IMG tag
140
	 * @return String containing either just a URL or a complete image tag
141
	 * @source http://gravatar.com/site/implement/images/php/
142
	 */
143
	public static function gravatar($email, $s = '80', $d = 'mm', $r = 'g', $img = false, $atts = array())
144
	{
145
		$out = '//www.gravatar.com/avatar/';
146
		$out .= md5(strtolower(trim($email)));
147
		$out .= "?s=$s&d=$d&r=$r";
148
		if ($img) {
149
			$out = Html::img($out, $atts);
150
		}
151
		return $out;
152
	}
153
154
	/**
155
	 * @inheritdoc
156
	 */
157
	public static function checkbox($name, $checked = false, $options = [])
158
	{
159
		$checkbox = parent::checkbox($name, $checked, $options);
160
		return '<div class="checkbox">' . $checkbox . '</div>';
161
	}
162
163
	/**
164
	 * @inheritdoc
165
	 */
166
	public static function radio($name, $checked = false, $options = [])
167
	{
168
		$radio = parent::radio($name, $checked, $options);
169
		return '<div class="radio">' . $radio . '</div>';
170
	}
171
172
	/**
173
	 * Sanitise a value.
174
	 *
175
	 * @param mixed $value
176
	 * @param string $allowableTags - see striptags function for the correct format
177
	 * @return mixed $value
178
	 */
179 82
	public static function sanitise($value, $allowableTags = null)
180
	{
181 82
		profile_begin('Html::sanitise', 'html');
182 82
		static $tagsProcessed = [];
183 82
		if (!empty($allowableTags) && !in_array($allowableTags, $tagsProcessed)) {
184 78
			self::processTags($allowableTags, $tagsProcessed);
0 ignored issues
show
Bug introduced by
$allowableTags of type string is incompatible with the type array expected by parameter $openingTags of neon\core\helpers\Html::processTags(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

184
			self::processTags(/** @scrutinizer ignore-type */ $allowableTags, $tagsProcessed);
Loading history...
185
		}
186 82
		if (is_array($value)) {
187 22
			foreach ($value as $k => $v)
188 22
				$value[$k] = static::sanitise($v, $allowableTags);
189
		} else {
190 82
			if (!is_null($value)) {
191 82
				if (isset($tagsProcessed[$allowableTags])) {
192 78
					$tags = $tagsProcessed[$allowableTags];
193 78
					$value = str_replace($tags['return'], $tags['replace'], $value);
194 78
					$value = trim(strip_tags($value));
195 78
					$value = str_replace($tags['replace'], $tags['return'], $value);
196
				} else {
197 4
					$value = trim(strip_tags($value));
198
				}
199
			}
200
		}
201 82
		profile_end('Html::sanitise', 'html');
202 82
		return $value;
203
	}
204
205
	/**
206
	 * Purify a value allow for some html parameters to get through. These can
207
	 * be further restricted from the default by using $allowedTags
208
	 *
209
	 * Note: this is slow in comparison to sanitise and should be used only when
210
	 * you are expecting more complex HTML to be passed in e.g. wysiwyg
211
	 *
212
	 * @param mixed $value
213
	 * @param string $allowedTags - see striptags function for the correct format
214
	 *   If you set this to false, tags will not be stripped beyond the
215
	 *   default ones by HTML::purifier
216
	 * @return mixed $value
217
	 */
218
	public static function purify($value, $allowedTags = null)
219
	{
220
		profile_begin('Html::purify', 'html');
221
		if (is_array($value)) {
222
			foreach ($value as $k => $v)
223
				$value[$k] = static::purify($v, $allowedTags);
224
		} else {
225
			if (!is_null($value)) {
226
				if ($allowedTags === false) {
0 ignored issues
show
introduced by
The condition $allowedTags === false is always false.
Loading history...
227
					$value = trim(HtmlPurifier::process($value, function($config) {
228
						$def = $config->getHTMLDefinition(true);
229
						$def->addAttribute('img', 'srcset', 'Text');
230
						$def->addAttribute('img', 'sizes', 'Text');
231
						$def->addAttribute('img', 'loading', 'Text');
232
					}));
233
				} else {
234
					$value = trim(HtmlPurifier::process(strip_tags($value, $allowedTags)));
235
				}
236
			}
237
		}
238
		profile_end('Html::purify', 'html');
239
		return $value;
240
	}
241
242
	/**
243
	 * Apply htmlentities recursively onto a value.
244
	 *
245
	 * @param mixed $value  the values to be encoded
246
	 * @param int $entities  which entities should be encoded - this is
247
	 *   a or'ed bit mask of required types (see htmlentities for details)
248
	 * @return string
249
	 */
250
	public static function encodeEntities($value, $entities = ENT_QUOTES)
251
	{
252
		if (is_array($value)) {
253
			foreach ($value as $k => $v)
254
				$value[$k] = static::encodeEntities($v, $entities);
255
		} else {
256
			$value = htmlentities($value, $entities);
257
		}
258
		return $value;
259
	}
260
261
	/**
262
	 * helper method to allow for the keeping of allowable tags in strip tags
263
	 * @param array $openingTags
264
	 * @param array $tagsProcessed
265
	 */
266 78
	protected static function processTags($openingTags, &$tagsProcessed)
267
	{
268
		// append the closing tags to the list and add a comma for splitting later
269 78
		$tagsStr = str_replace('>','>,',($openingTags.str_replace('<','</',$openingTags)));
0 ignored issues
show
Bug introduced by
Are you sure $openingTags of type array can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

269
		$tagsStr = str_replace('>','>,',(/** @scrutinizer ignore-type */ $openingTags.str_replace('<','</',$openingTags)));
Loading history...
270
		// create the replacement code with a highly unlikely set of strings
271 78
		$replaceStr = str_replace(['</', '<', '/>', '>'], ['__NEWICON_CLOSELT__', '__NEWICON_LT__', '__NEWICON_CLOSERT__', '__NEWICON_RT__'], $tagsStr);
272
273
		// now create the replacement and returned tags
274 78
		$replaceTags = explode(',', substr($replaceStr,0,-1)); // ignore trailing comma
275 78
		$returnTags = explode(',', substr($tagsStr,0,-1)); // ignore trailing comma
276 78
		$tagsProcessed[$openingTags] = ['replace'=>$replaceTags, 'return'=>$returnTags];
277 78
	}
278
279
	/**
280
	 * Wrap text in a highlight span tag with class neonBgHighlight
281
	 * This function will not break html tags when finding and replacing text
282
	 *
283
	 * @param string|array $search - a non html string or array of strings to search for
284
	 * @param string $html - html content on which to highlight the string
285
	 * @param string $empty - what to show if the field is empty
286
	 * @return string html
287
	 */
288 12
	public static function highlight($search, $html, $empty='')
289
	{
290 12
		if ($html === null)
0 ignored issues
show
introduced by
The condition $html === null is always false.
Loading history...
291
			return neon()->formatter->nullDisplay;
292 12
		if ($html === '')
293
			return $empty;
294 12
		if (empty($search))
295
			return $html;
296
297 12
		profile_begin('Html::Highlight');
298
		// escape search and html entities as otherwise the loadHTML or appendXML
299
		// can blow up if there are < & > in the data
300 12
		if (!is_array($search))
301 12
			$search = [$search];
302
		try {
303 12
			foreach ($search as $s) {
304
				// now search and rebuild the data
305 12
				$dom = new DOMDocument('1.0', 'UTF-8');
306
				// suppress all warning!
307
				// loadHtml seems to ignore the LIBXML_ERR_NONE setting
308
				// we don;t care if we are not given perfectly formed html
309
				// Even if we have serious issues in the html `$dom->saveHtml` makes a good expected effort
310 12
				@$dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED + LIBXML_HTML_NODEFDTD + LIBXML_NONET + LIBXML_ERR_NONE);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for loadHTML(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

310
				/** @scrutinizer ignore-unhandled */ @$dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED + LIBXML_HTML_NODEFDTD + LIBXML_NONET + LIBXML_ERR_NONE);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
311 12
				$xpath = new DOMXPath($dom);
312 12
				$s = htmlentities($s);
313 12
				$findStack = [];
314
				// Only operate a string find / replace on the content of html text nodes
315 12
				foreach ($xpath->query('//text()') as $node) {
316 12
					$f = $dom->createDocumentFragment();
317
					// appendXml is useless and will break easily when exposed to characters
318
					// preg_replace is a million times more robust....
319
					// So keep the dom parser happy by replacing its internal text with an md5 key
320 12
					$replace = preg_replace('/(' . preg_quote($s) . ')/i', '<span class="neonSearchHighlight">$1</span>', htmlentities($node->nodeValue));
321 12
					$key = '{[' . md5($replace) . ']}';
322 12
					$findStack[$key] = $replace;
323 12
					$f->appendXML($key);
324 12
					$node->parentNode->replaceChild($f, $node);
325
				}
326 12
				$html = $dom->saveHTML();
327
				// additional string to replace
328 12
				$findStack["\n"] = '';
329 12
				$findStack['<?xml encoding="utf-8" ?>'] = '';
330
				// replace keys with the results of the search and replace
331 12
				$html = str_replace(array_keys($findStack), array_values($findStack), $html);
332
			}
333
		} catch (Exception $e) {
334
			return $html;
335
		}
336 12
		profile_end('Html::Highlight');
337 12
		return $html;
338
	}
339
340
}
341