Passed
Push — develop ( 03421b...c9efed )
by steve
39:12 queued 25:46
created

Html::highlight()   B

Complexity

Conditions 8
Paths 51

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 8.2685

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 31
dl 0
loc 50
rs 8.1795
c 3
b 0
f 0
cc 8
nc 51
nop 3
ccs 26
cts 31
cp 0.8387
crap 8.2685
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
	 * Return the Html for a custom button
30
	 *
31
	 * @param mixed $url  the URL the button should return to @see \yii\helpers\Url::to
32
	 * @param string $text  the button text
33
	 * @param string $icon  the fa icon to use (can omit the fa-)
34
	 * @param string $class  the class(es) to apply post class btn
35
	 * @param array $options  any additional options
36
	 *  - specify $options['icon']
37
	 * @return string Html for the custom button
38
	 */
39
	public static function buttonCustom($url, $text, array $options=[])
40
	{
41
		$options['class'] = 'btn '. Arr::get($options, 'class', '');
42
		$icon = Arr::remove($options, 'icon', '');
43
		return self::a("<i class=\"$icon\"></i> ".$text, $url, $options);
44
	}
45
46
	/**
47
	 * Return the Html for a standard edit button
48
	 *
49
	 * @param array|string $url @see \yii\helpers\Url::to
50
	 * @param string $text - button text
51
	 * @return string Html for an edit button
52
	 */
53
	public static function buttonEdit($url, $text='Edit', $options=[])
54
	{
55
		$options['icon'] = 'fa fa-pencil';
56
		$options['class'] = 'btn-default ' . Arr::get($options, 'class', '');
57
		return self::buttonCustom($url, $text, $options);
58
	}
59
60
	/**
61
	 * Return the HTML for a standard delete button
62
	 * Note delete often means soft delete (destroy means remove from the db entirely)
63
	 *
64
	 * @param mixed $url
65
	 * @see \yii\helpers\Url::to
66
	 * @return string Html
67
	 */
68
	public static function buttonDelete($url, $text='Delete', $options=[])
69
	{
70
		$options['icon'] = 'fa fa-trash';
71
		$options['class'] =  'btn-danger ' . Arr::get($options, 'class', '');
72
		return self::buttonCustom($url, $text, $options);
73
	}
74
75
	/**
76
	 * Return the HTML for a standard Add button
77
	 *
78
	 * @param mixed $url
79
	 * @param string $text button text
80
	 * @see \yii\helpers\Url::to
81
	 * @return string Html
82
	 */
83
	public static function buttonAdd($url, $text='Add', $options=[])
84
	{
85
		$options['icon'] = 'fa fa-plus';
86
		$options['class'] = 'btn-primary btn-add';
87
		return self::buttonCustom($url, $text, $options);
88
	}
89
90
	/**
91
	 * Return the HTML for a standard cancel button
92
	 *
93
	 * @param mixed $url
94
	 * @param string $text button text
95
	 * @return string Html
96
	 */
97
	public static function buttonCancel($url='', $text='Cancel', $options=[])
98
	{
99
		$options['icon'] = 'fa fa-arrow-left';
100
		$options['class'] = 'btn-default btn-cancel';
101
		return self::buttonCustom(Url::goBack($url), $text, $options);
102
	}
103
104
	/**
105
	 * Get either a Gravatar URL or complete image tag for a specified email address.
106
	 *
107
	 * @param string $email The email address
108
	 * @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ]
109
	 * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
110
	 * @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
111
	 * @param bool $img True to return a complete IMG tag False for just the URL
112
	 * @param array $atts Optional, additional key/value attributes to include in the IMG tag
113
	 * @return String containing either just a URL or a complete image tag
114
	 * @source http://gravatar.com/site/implement/images/php/
115
	 */
116
	public static function gravatar($email, $s = '80', $d = 'mm', $r = 'g', $img = false, $atts = array())
117
	{
118
		$out = '//www.gravatar.com/avatar/';
119
		$out .= md5(strtolower(trim($email)));
120
		$out .= "?s=$s&d=$d&r=$r";
121
		if ($img) {
122
			$out = Html::img($out, $atts);
123
		}
124
		return $out;
125
	}
126
127
	/**
128
	 * @inheritdoc
129
	 */
130
	public static function checkbox($name, $checked = false, $options = [])
131
	{
132
		$checkbox = parent::checkbox($name, $checked, $options);
133
		return '<div class="checkbox">' . $checkbox . '</div>';
134
	}
135
136
	/**
137
	 * @inheritdoc
138
	 */
139
	public static function radio($name, $checked = false, $options = [])
140
	{
141
		$radio = parent::radio($name, $checked, $options);
142
		return '<div class="radio">' . $radio . '</div>';
143
	}
144
145
	/**
146
	 * Sanitise a value.
147
	 *
148
	 * @param mixed $value
149
	 * @param string $allowableTags - see striptags function for the correct format
150
	 * @return mixed $value
151
	 */
152 82
	public static function sanitise($value, $allowableTags = null)
153
	{
154 82
		profile_begin('Html::sanitise', 'html');
155 82
		static $tagsProcessed = [];
156 82
		if (!empty($allowableTags) && !in_array($allowableTags, $tagsProcessed)) {
157 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

157
			self::processTags(/** @scrutinizer ignore-type */ $allowableTags, $tagsProcessed);
Loading history...
158
		}
159 82
		if (is_array($value)) {
160 22
			foreach ($value as $k => $v)
161 22
				$value[$k] = static::sanitise($v, $allowableTags);
162
		} else {
163 82
			if (!is_null($value)) {
164 82
				if (isset($tagsProcessed[$allowableTags])) {
165 78
					$tags = $tagsProcessed[$allowableTags];
166 78
					$value = str_replace($tags['return'], $tags['replace'], $value);
167 78
					$value = trim(strip_tags($value));
168 78
					$value = str_replace($tags['replace'], $tags['return'], $value);
169
				} else {
170 4
					$value = trim(strip_tags($value));
171
				}
172
			}
173
		}
174 82
		profile_end('Html::sanitise', 'html');
175 82
		return $value;
176
	}
177
178
	/**
179
	 * Purify a value allow for some html parameters to get through. These can
180
	 * be further restricted from the default by using $allowedTags
181
	 *
182
	 * Note: this is slow in comparison to sanitise and should be used only when
183
	 * you are expecting more complex HTML to be passed in e.g. wysiwyg
184
	 *
185
	 * @param mixed $value
186
	 * @param string $allowedTags - see striptags function for the correct format
187
	 *   If you set this to false, tags will not be stripped beyond the
188
	 *   default ones by HTML::purifier
189
	 * @return mixed $value
190
	 */
191
	public static function purify($value, $allowedTags = null)
192
	{
193
		profile_begin('Html::purify', 'html');
194
		if (is_array($value)) {
195
			foreach ($value as $k => $v)
196
				$value[$k] = static::purify($v, $allowedTags);
197
		} else {
198
			if (!is_null($value)) {
199
				if ($allowedTags === false) {
0 ignored issues
show
introduced by
The condition $allowedTags === false is always false.
Loading history...
200
					$value = trim(HtmlPurifier::process($value, function($config) {
201
						$def = $config->getHTMLDefinition(true);
202
						$def->addAttribute('img', 'srcset', 'Text');
203
						$def->addAttribute('img', 'sizes', 'Text');
204
						$def->addAttribute('img', 'loading', 'Text');
205
					}));
206
				} else {
207
					$value = trim(HtmlPurifier::process(strip_tags($value, $allowedTags)));
208
				}
209
			}
210
		}
211
		profile_end('Html::purify', 'html');
212
		return $value;
213
	}
214
215
	/**
216
	 * Apply htmlentities recursively onto a value.
217
	 *
218
	 * @param mixed $value  the values to be encoded
219
	 * @param int $entities  which entities should be encoded - this is
220
	 *   a or'ed bit mask of required types (see htmlentities for details)
221
	 * @return string
222
	 */
223
	public static function encodeEntities($value, $entities = ENT_QUOTES)
224
	{
225
		if (is_array($value)) {
226
			foreach ($value as $k => $v)
227
				$value[$k] = static::encodeEntities($v, $entities);
228
		} else {
229
			$value = htmlentities($value, $entities);
230
		}
231
		return $value;
232
	}
233
234
	/**
235
	 * helper method to allow for the keeping of allowable tags in strip tags
236
	 * @param array $openingTags
237
	 * @param array $tagsProcessed
238
	 */
239 78
	protected static function processTags($openingTags, &$tagsProcessed)
240
	{
241
		// append the closing tags to the list and add a comma for splitting later
242 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

242
		$tagsStr = str_replace('>','>,',(/** @scrutinizer ignore-type */ $openingTags.str_replace('<','</',$openingTags)));
Loading history...
243
		// create the replacement code with a highly unlikely set of strings
244 78
		$replaceStr = str_replace(['</', '<', '/>', '>'], ['__NEWICON_CLOSELT__', '__NEWICON_LT__', '__NEWICON_CLOSERT__', '__NEWICON_RT__'], $tagsStr);
245
246
		// now create the replacement and returned tags
247 78
		$replaceTags = explode(',', substr($replaceStr,0,-1)); // ignore trailing comma
248 78
		$returnTags = explode(',', substr($tagsStr,0,-1)); // ignore trailing comma
249 78
		$tagsProcessed[$openingTags] = ['replace'=>$replaceTags, 'return'=>$returnTags];
250 78
	}
251
252
	/**
253
	 * Wrap text in a highlight span tag with class neonBgHighlight
254
	 * This function will not break html tags when finding and replacing text
255
	 *
256
	 * @param string|array $search - a non html string or array of strings to search for
257
	 * @param string $html - html content on which to highlight the string
258
	 * @param string $empty - what to show if the field is empty
259
	 * @return string html
260
	 */
261 12
	public static function highlight($search, $html, $empty='')
262
	{
263 12
		if ($html === null)
0 ignored issues
show
introduced by
The condition $html === null is always false.
Loading history...
264
			return neon()->formatter->nullDisplay;
265 12
		if ($html === '')
266
			return $empty;
267 12
		if (empty($search))
268
			return $html;
269
270 12
		profile_begin('Html::Highlight');
271
		// escape search and html entities as otherwise the loadHTML or appendXML
272
		// can blow up if there are < & > in the data
273 12
		if (!is_array($search))
274 12
			$search = [$search];
275
		try {
276 12
			foreach ($search as $s) {
277
				// now search and rebuild the data
278 12
				$dom = new DOMDocument('1.0', 'UTF-8');
279
				// suppress all warning!
280
				// loadHtml seems to ignore the LIBXML_ERR_NONE setting
281
				// we don;t care if we are not given perfectly formed html
282
				// Even if we have serious issues in the html `$dom->saveHtml` makes a good expected effort
283 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

283
				/** @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...
284 12
				$xpath = new DOMXPath($dom);
285 12
				$s = htmlentities($s);
286 12
				$findStack = [];
287
				// Only operate a string find / replace on the content of html text nodes
288 12
				foreach ($xpath->query('//text()') as $node) {
289 12
					$f = $dom->createDocumentFragment();
290
					// appendXml is useless and will break easily when exposed to characters
291
					// preg_replace is a million times more robust....
292
					// So keep the dom parser happy by replacing its internal text with an md5 key
293 12
					$replace = preg_replace('/(' . preg_quote($s) . ')/i', '<span class="neonSearchHighlight">$1</span>', htmlentities($node->nodeValue));
294 12
					$key = '{[' . md5($replace) . ']}';
295 12
					$findStack[$key] = $replace;
296 12
					$f->appendXML($key);
297 12
					$node->parentNode->replaceChild($f, $node);
298
				}
299 12
				$html = $dom->saveHTML();
300
				// additional string to replace
301 12
				$findStack["\n"] = '';
302 12
				$findStack['<?xml encoding="utf-8" ?>'] = '';
303
				// replace keys with the results of the search and replace
304 12
				$html = str_replace(array_keys($findStack), array_values($findStack), $html);
305
			}
306
		} catch (Exception $e) {
307
			return $html;
308
		}
309 12
		profile_end('Html::Highlight');
310 12
		return $html;
311
	}
312
313
}
314