PageRenderTime 67ms CodeModel.GetById 11ms app.highlight 47ms RepoModel.GetById 1ms app.codeStats 1ms

/libraries/joomla/filter/input.php

https://gitlab.com/vitaliylukin91/text
PHP | 1089 lines | 616 code | 137 blank | 336 comment | 106 complexity | 497aca35b4091ec71a3d15fa04e6592f MD5 | raw file
   1<?php
   2/**
   3 * @package     Joomla.Platform
   4 * @subpackage  Filter
   5 *
   6 * @copyright   Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
   7 * @license     GNU General Public License version 2 or later; see LICENSE
   8 */
   9
  10defined('JPATH_PLATFORM') or die;
  11
  12/**
  13 * JFilterInput is a class for filtering input from any data source
  14 *
  15 * Forked from the php input filter library by: Daniel Morris <dan@rootcube.com>
  16 * Original Contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie.
  17 *
  18 * @since  11.1
  19 */
  20class JFilterInput
  21{
  22	/**
  23	 * A container for JFilterInput instances.
  24	 *
  25	 * @var    array
  26	 * @since  11.3
  27	 */
  28	protected static $instances = array();
  29
  30	/**
  31	 * The array of permitted tags (white list).
  32	 *
  33	 * @var    array
  34	 * @since  11.1
  35	 */
  36	public $tagsArray;
  37
  38	/**
  39	 * The array of permitted tag attributes (white list).
  40	 *
  41	 * @var    array
  42	 * @since  11.1
  43	 */
  44	public $attrArray;
  45
  46	/**
  47	 * The method for sanitising tags: WhiteList method = 0 (default), BlackList method = 1
  48	 *
  49	 * @var    integer
  50	 * @since  11.1
  51	 */
  52	public $tagsMethod;
  53
  54	/**
  55	 * The method for sanitising attributes: WhiteList method = 0 (default), BlackList method = 1
  56	 *
  57	 * @var    integer
  58	 * @since  11.1
  59	 */
  60	public $attrMethod;
  61
  62	/**
  63	 * A flag for XSS checks. Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1
  64	 *
  65	 * @var    integer
  66	 * @since  11.1
  67	 */
  68	public $xssAuto;
  69
  70	/**
  71	 * The list of the default blacklisted tags.
  72	 *
  73	 * @var    array
  74	 * @since  11.1
  75	 */
  76	public $tagBlacklist = array(
  77		'applet',
  78		'body',
  79		'bgsound',
  80		'base',
  81		'basefont',
  82		'embed',
  83		'frame',
  84		'frameset',
  85		'head',
  86		'html',
  87		'id',
  88		'iframe',
  89		'ilayer',
  90		'layer',
  91		'link',
  92		'meta',
  93		'name',
  94		'object',
  95		'script',
  96		'style',
  97		'title',
  98		'xml'
  99	);
 100
 101	/**
 102	 * The list of the default blacklisted tag attributes. All event handlers implicit.
 103	 *
 104	 * @var    array
 105	 * @since   11.1
 106	 */
 107	public $attrBlacklist = array(
 108		'action',
 109		'background',
 110		'codebase',
 111		'dynsrc',
 112		'lowsrc'
 113	);
 114
 115	/**
 116	 * Constructor for inputFilter class. Only first parameter is required.
 117	 *
 118	 * @param   array    $tagsArray   List of user-defined tags
 119	 * @param   array    $attrArray   List of user-defined attributes
 120	 * @param   integer  $tagsMethod  WhiteList method = 0, BlackList method = 1
 121	 * @param   integer  $attrMethod  WhiteList method = 0, BlackList method = 1
 122	 * @param   integer  $xssAuto     Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1
 123	 *
 124	 * @since   11.1
 125	 */
 126	public function __construct($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1)
 127	{
 128		// Make sure user defined arrays are in lowercase
 129		$tagsArray = array_map('strtolower', (array) $tagsArray);
 130		$attrArray = array_map('strtolower', (array) $attrArray);
 131
 132		// Assign member variables
 133		$this->tagsArray = $tagsArray;
 134		$this->attrArray = $attrArray;
 135		$this->tagsMethod = $tagsMethod;
 136		$this->attrMethod = $attrMethod;
 137		$this->xssAuto = $xssAuto;
 138	}
 139
 140	/**
 141	 * Returns an input filter object, only creating it if it doesn't already exist.
 142	 *
 143	 * @param   array    $tagsArray   List of user-defined tags
 144	 * @param   array    $attrArray   List of user-defined attributes
 145	 * @param   integer  $tagsMethod  WhiteList method = 0, BlackList method = 1
 146	 * @param   integer  $attrMethod  WhiteList method = 0, BlackList method = 1
 147	 * @param   integer  $xssAuto     Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1
 148	 *
 149	 * @return  JFilterInput  The JFilterInput object.
 150	 *
 151	 * @since   11.1
 152	 */
 153	public static function &getInstance($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1)
 154	{
 155		$sig = md5(serialize(array($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto)));
 156
 157		if (empty(self::$instances[$sig]))
 158		{
 159			self::$instances[$sig] = new JFilterInput($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto);
 160		}
 161
 162		return self::$instances[$sig];
 163	}
 164
 165	/**
 166	 * Method to be called by another php script. Processes for XSS and
 167	 * specified bad code.
 168	 *
 169	 * @param   mixed   $source  Input string/array-of-string to be 'cleaned'
 170	 * @param   string  $type    The return type for the variable:
 171	 *                           INT:       An integer,
 172	 *                           UINT:      An unsigned integer,
 173	 *                           FLOAT:     A floating point number,
 174	 *                           BOOLEAN:   A boolean value,
 175	 *                           WORD:      A string containing A-Z or underscores only (not case sensitive),
 176	 *                           ALNUM:     A string containing A-Z or 0-9 only (not case sensitive),
 177	 *                           CMD:       A string containing A-Z, 0-9, underscores, periods or hyphens (not case sensitive),
 178	 *                           BASE64:    A string containing A-Z, 0-9, forward slashes, plus or equals (not case sensitive),
 179	 *                           STRING:    A fully decoded and sanitised string (default),
 180	 *                           HTML:      A sanitised string,
 181	 *                           ARRAY:     An array,
 182	 *                           PATH:      A sanitised file path,
 183	 *                           TRIM:      A string trimmed from normal, non-breaking and multibyte spaces
 184	 *                           USERNAME:  Do not use (use an application specific filter),
 185	 *                           RAW:       The raw string is returned with no filtering,
 186	 *                           unknown:   An unknown filter will act like STRING. If the input is an array it will return an
 187	 *                                      array of fully decoded and sanitised strings.
 188	 *
 189	 * @return  mixed  'Cleaned' version of input parameter
 190	 *
 191	 * @since   11.1
 192	 */
 193	public function clean($source, $type = 'string')
 194	{
 195		// Handle the type constraint
 196		switch (strtoupper($type))
 197		{
 198			case 'INT':
 199			case 'INTEGER':
 200				// Only use the first integer value
 201				preg_match('/-?[0-9]+/', (string) $source, $matches);
 202				$result = @ (int) $matches[0];
 203				break;
 204
 205			case 'UINT':
 206				// Only use the first integer value
 207				preg_match('/-?[0-9]+/', (string) $source, $matches);
 208				$result = @ abs((int) $matches[0]);
 209				break;
 210
 211			case 'FLOAT':
 212			case 'DOUBLE':
 213				// Only use the first floating point value
 214				preg_match('/-?[0-9]+(\.[0-9]+)?/', (string) $source, $matches);
 215				$result = @ (float) $matches[0];
 216				break;
 217
 218			case 'BOOL':
 219			case 'BOOLEAN':
 220				$result = (bool) $source;
 221				break;
 222
 223			case 'WORD':
 224				$result = (string) preg_replace('/[^A-Z_]/i', '', $source);
 225				break;
 226
 227			case 'ALNUM':
 228				$result = (string) preg_replace('/[^A-Z0-9]/i', '', $source);
 229				break;
 230
 231			case 'CMD':
 232				$result = (string) preg_replace('/[^A-Z0-9_\.-]/i', '', $source);
 233				$result = ltrim($result, '.');
 234				break;
 235
 236			case 'BASE64':
 237				$result = (string) preg_replace('/[^A-Z0-9\/+=]/i', '', $source);
 238				break;
 239
 240			case 'STRING':
 241				$result = (string) $this->_remove($this->_decode((string) $source));
 242				break;
 243
 244			case 'HTML':
 245				$result = (string) $this->_remove((string) $source);
 246				break;
 247
 248			case 'ARRAY':
 249				$result = (array) $source;
 250				break;
 251
 252			case 'PATH':
 253				$pattern = '/^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/][A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$/';
 254				preg_match($pattern, (string) $source, $matches);
 255				$result = @ (string) $matches[0];
 256				break;
 257
 258			case 'TRIM':
 259				$result = (string) trim($source);
 260				$result = JString::trim($result, chr(0xE3) . chr(0x80) . chr(0x80));
 261				$result = JString::trim($result, chr(0xC2) . chr(0xA0));
 262				break;
 263
 264			case 'USERNAME':
 265				$result = (string) preg_replace('/[\x00-\x1F\x7F<>"\'%&]/', '', $source);
 266				break;
 267
 268			case 'RAW':
 269				$result = $source;
 270				break;
 271
 272			default:
 273				// Are we dealing with an array?
 274				if (is_array($source))
 275				{
 276					foreach ($source as $key => $value)
 277					{
 278						// Filter element for XSS and other 'bad' code etc.
 279						if (is_string($value))
 280						{
 281							$source[$key] = $this->_remove($this->_decode($value));
 282						}
 283					}
 284
 285					$result = $source;
 286				}
 287				else
 288				{
 289					// Or a string?
 290					if (is_string($source) && !empty($source))
 291					{
 292						// Filter source for XSS and other 'bad' code etc.
 293						$result = $this->_remove($this->_decode($source));
 294					}
 295					else
 296					{
 297						// Not an array or string.. return the passed parameter
 298						$result = $source;
 299					}
 300				}
 301				break;
 302		}
 303
 304		return $result;
 305	}
 306
 307	/**
 308	 * Function to determine if contents of an attribute are safe
 309	 *
 310	 * @param   array  $attrSubSet  A 2 element array for attribute's name, value
 311	 *
 312	 * @return  boolean  True if bad code is detected
 313	 *
 314	 * @since   11.1
 315	 */
 316	public static function checkAttribute($attrSubSet)
 317	{
 318		$attrSubSet[0] = strtolower($attrSubSet[0]);
 319		$attrSubSet[1] = strtolower($attrSubSet[1]);
 320
 321		return (((strpos($attrSubSet[1], 'expression') !== false) && ($attrSubSet[0]) == 'style') || (strpos($attrSubSet[1], 'javascript:') !== false) ||
 322			(strpos($attrSubSet[1], 'behaviour:') !== false) || (strpos($attrSubSet[1], 'vbscript:') !== false) ||
 323			(strpos($attrSubSet[1], 'mocha:') !== false) || (strpos($attrSubSet[1], 'livescript:') !== false));
 324	}
 325
 326	/**
 327	 * Checks an uploaded for suspicious naming and potential PHP contents which could indicate a hacking attempt.
 328	 *
 329	 * The options you can define are:
 330	 * null_byte                   Prevent files with a null byte in their name (buffer overflow attack)
 331	 * forbidden_extensions        Do not allow these strings anywhere in the file's extension
 332	 * php_tag_in_content          Do not allow <?php tag in content
 333	 * shorttag_in_content         Do not allow short tag <? in content
 334	 * shorttag_extensions         Which file extensions to scan for short tags in content
 335	 * fobidden_ext_in_content     Do not allow forbidden_extensions anywhere in content
 336	 * php_ext_content_extensions  Which file extensions to scan for .php in content
 337	 *
 338	 * This code is an adaptation and improvement of Admin Tools' UploadShield feature,
 339	 * relicensed and contributed by its author.
 340	 *
 341	 * @param   array  $file     An uploaded file descriptor
 342	 * @param   array  $options  The scanner options (see the code for details)
 343	 *
 344	 * @return  boolean  True of the file is safe
 345	 *
 346	 * @since   3.4
 347	 */
 348	public static function isSafeFile($file, $options = array())
 349	{
 350		$defaultOptions = array(
 351			// Null byte in file name
 352			'null_byte'                  => true,
 353			// Forbidden string in extension (e.g. php matched .php, .xxx.php, .php.xxx and so on)
 354			'forbidden_extensions'       => array(
 355				'php', 'phps', 'php5', 'php3', 'php4', 'inc', 'pl', 'cgi', 'fcgi', 'java', 'jar', 'py'
 356			),
 357			// <?php tag in file contents
 358			'php_tag_in_content'         => true,
 359			// <? tag in file contents
 360			'shorttag_in_content'        => true,
 361			// Which file extensions to scan for short tags
 362			'shorttag_extensions'        => array(
 363				'inc', 'phps', 'class', 'php3', 'php4', 'php5', 'txt', 'dat', 'tpl', 'tmpl'
 364			),
 365			// Forbidden extensions anywhere in the content
 366			'fobidden_ext_in_content'    => true,
 367			// Which file extensions to scan for .php in the content
 368			'php_ext_content_extensions' => array('zip', 'rar', 'tar', 'gz', 'tgz', 'bz2', 'tbz', 'jpa'),
 369		);
 370
 371		$options = array_merge($defaultOptions, $options);
 372
 373		// Make sure we can scan nested file descriptors
 374		$descriptors = $file;
 375
 376		if (isset($file['name']) && isset($file['tmp_name']))
 377		{
 378			$descriptors = self::decodeFileData(
 379				array(
 380					$file['name'],
 381					$file['type'],
 382					$file['tmp_name'],
 383					$file['error'],
 384					$file['size']
 385				)
 386			);
 387		}
 388
 389		// Handle non-nested descriptors (single files)
 390		if (isset($descriptors['name']))
 391		{
 392			$descriptors = array($descriptors);
 393		}
 394
 395		// Scan all descriptors detected
 396		foreach ($descriptors as $fileDescriptor)
 397		{
 398			if (!isset($fileDescriptor['name']))
 399			{
 400				// This is a nested descriptor. We have to recurse.
 401				if (!self::isSafeFile($fileDescriptor, $options))
 402				{
 403					return false;
 404				}
 405
 406				continue;
 407			}
 408
 409			$tempNames     = $fileDescriptor['tmp_name'];
 410			$intendedNames = $fileDescriptor['name'];
 411
 412			if (!is_array($tempNames))
 413			{
 414				$tempNames = array($tempNames);
 415			}
 416
 417			if (!is_array($intendedNames))
 418			{
 419				$intendedNames = array($intendedNames);
 420			}
 421
 422			$len = count($tempNames);
 423
 424			for ($i = 0; $i < $len; $i++)
 425			{
 426				$tempName     = array_shift($tempNames);
 427				$intendedName = array_shift($intendedNames);
 428
 429				// 1. Null byte check
 430				if ($options['null_byte'])
 431				{
 432					if (strstr($intendedName, "\x00"))
 433					{
 434						return false;
 435					}
 436				}
 437
 438				// 2. PHP-in-extension check (.php, .php.xxx[.yyy[.zzz[...]]], .xxx[.yyy[.zzz[...]]].php)
 439				if (!empty($options['forbidden_extensions']))
 440				{
 441					$explodedName = explode('.', $intendedName);
 442					$explodedName =	array_reverse($explodedName);
 443					array_pop($explodedName);
 444					array_map('strtolower', $explodedName);
 445
 446					/*
 447					 * DO NOT USE array_intersect HERE! array_intersect expects the two arrays to
 448					 * be set, i.e. they should have unique values.
 449					 */
 450					foreach ($options['forbidden_extensions'] as $ext)
 451					{
 452						if (in_array($ext, $explodedName))
 453						{
 454							return false;
 455						}
 456					}
 457				}
 458
 459				// 3. File contents scanner (PHP tag in file contents)
 460				if ($options['php_tag_in_content'] || $options['shorttag_in_content']
 461					|| ($options['fobidden_ext_in_content'] && !empty($options['forbidden_extensions'])))
 462				{
 463					$fp = @fopen($tempName, 'r');
 464
 465					if ($fp !== false)
 466					{
 467						$data = '';
 468
 469						while (!feof($fp))
 470						{
 471							$buffer = @fread($fp, 131072);
 472							$data .= $buffer;
 473
 474							if ($options['php_tag_in_content'] && strstr($buffer, '<?php'))
 475							{
 476								return false;
 477							}
 478
 479							if ($options['shorttag_in_content'])
 480							{
 481								$suspiciousExtensions = $options['shorttag_extensions'];
 482
 483								if (empty($suspiciousExtensions))
 484								{
 485									$suspiciousExtensions = array(
 486										'inc', 'phps', 'class', 'php3', 'php4', 'txt', 'dat', 'tpl', 'tmpl'
 487									);
 488								}
 489
 490								/*
 491								 * DO NOT USE array_intersect HERE! array_intersect expects the two arrays to
 492								 * be set, i.e. they should have unique values.
 493								 */
 494								$collide = false;
 495
 496								foreach ($suspiciousExtensions as $ext)
 497								{
 498									if (in_array($ext, $explodedName))
 499									{
 500										$collide = true;
 501
 502										break;
 503									}
 504								}
 505
 506								if ($collide)
 507								{
 508									// These are suspicious text files which may have the short tag (<?) in them
 509									if (strstr($buffer, '<?'))
 510									{
 511										return false;
 512									}
 513								}
 514							}
 515
 516							if ($options['fobidden_ext_in_content'] && !empty($options['forbidden_extensions']))
 517							{
 518								$suspiciousExtensions = $options['php_ext_content_extensions'];
 519
 520								if (empty($suspiciousExtensions))
 521								{
 522									$suspiciousExtensions = array(
 523										'zip', 'rar', 'tar', 'gz', 'tgz', 'bz2', 'tbz', 'jpa'
 524									);
 525								}
 526
 527								/*
 528								 * DO NOT USE array_intersect HERE! array_intersect expects the two arrays to
 529								 * be set, i.e. they should have unique values.
 530								 */
 531								$collide = false;
 532
 533								foreach ($suspiciousExtensions as $ext)
 534								{
 535									if (in_array($ext, $explodedName))
 536									{
 537										$collide = true;
 538
 539										break;
 540									}
 541								}
 542
 543								if ($collide)
 544								{
 545									/*
 546									 * These are suspicious text files which may have an executable
 547									 * file extension in them
 548									 */
 549									foreach ($options['forbidden_extensions'] as $ext)
 550									{
 551										if (strstr($buffer, '.' . $ext))
 552										{
 553											return false;
 554										}
 555									}
 556								}
 557							}
 558
 559							/*
 560							 * This makes sure that we don't accidentally skip a <?php tag if it's across
 561							 * a read boundary, even on multibyte strings
 562							 */
 563							$data = substr($data, -8);
 564						}
 565
 566						fclose($fp);
 567					}
 568				}
 569			}
 570		}
 571
 572		return true;
 573	}
 574
 575	/**
 576	 * Method to decode a file data array.
 577	 *
 578	 * @param   array  $data  The data array to decode.
 579	 *
 580	 * @return  array
 581	 *
 582	 * @since   3.4
 583	 */
 584	protected static function decodeFileData(array $data)
 585	{
 586		$result = array();
 587
 588		if (is_array($data[0]))
 589		{
 590			foreach ($data[0] as $k => $v)
 591			{
 592				$result[$k] = self::decodeFileData(array($data[0][$k], $data[1][$k], $data[2][$k], $data[3][$k], $data[4][$k]));
 593			}
 594
 595			return $result;
 596		}
 597
 598		return array('name' => $data[0], 'type' => $data[1], 'tmp_name' => $data[2], 'error' => $data[3], 'size' => $data[4]);
 599	}
 600
 601	/**
 602	 * Internal method to iteratively remove all unwanted tags and attributes
 603	 *
 604	 * @param   string  $source  Input string to be 'cleaned'
 605	 *
 606	 * @return  string  'Cleaned' version of input parameter
 607	 *
 608	 * @since   11.1
 609	 */
 610	protected function _remove($source)
 611	{
 612		$loopCounter = 0;
 613
 614		// Iteration provides nested tag protection
 615		while ($source != $this->_cleanTags($source))
 616		{
 617			$source = $this->_cleanTags($source);
 618			$loopCounter++;
 619		}
 620
 621		return $source;
 622	}
 623
 624	/**
 625	 * Internal method to strip a string of certain tags
 626	 *
 627	 * @param   string  $source  Input string to be 'cleaned'
 628	 *
 629	 * @return  string  'Cleaned' version of input parameter
 630	 *
 631	 * @since   11.1
 632	 */
 633	protected function _cleanTags($source)
 634	{
 635		// First, pre-process this for illegal characters inside attribute values
 636		$source = $this->_escapeAttributeValues($source);
 637
 638		// In the beginning we don't really have a tag, so everything is postTag
 639		$preTag = null;
 640		$postTag = $source;
 641		$currentSpace = false;
 642
 643		// Setting to null to deal with undefined variables
 644		$attr = '';
 645
 646		// Is there a tag? If so it will certainly start with a '<'.
 647		$tagOpen_start = strpos($source, '<');
 648
 649		while ($tagOpen_start !== false)
 650		{
 651			// Get some information about the tag we are processing
 652			$preTag .= substr($postTag, 0, $tagOpen_start);
 653			$postTag = substr($postTag, $tagOpen_start);
 654			$fromTagOpen = substr($postTag, 1);
 655			$tagOpen_end = strpos($fromTagOpen, '>');
 656
 657			// Check for mal-formed tag where we have a second '<' before the first '>'
 658			$nextOpenTag = (strlen($postTag) > $tagOpen_start) ? strpos($postTag, '<', $tagOpen_start + 1) : false;
 659
 660			if (($nextOpenTag !== false) && ($nextOpenTag < $tagOpen_end))
 661			{
 662				// At this point we have a mal-formed tag -- remove the offending open
 663				$postTag = substr($postTag, 0, $tagOpen_start) . substr($postTag, $tagOpen_start + 1);
 664				$tagOpen_start = strpos($postTag, '<');
 665				continue;
 666			}
 667
 668			// Let's catch any non-terminated tags and skip over them
 669			if ($tagOpen_end === false)
 670			{
 671				$postTag = substr($postTag, $tagOpen_start + 1);
 672				$tagOpen_start = strpos($postTag, '<');
 673				continue;
 674			}
 675
 676			// Do we have a nested tag?
 677			$tagOpen_nested = strpos($fromTagOpen, '<');
 678
 679			if (($tagOpen_nested !== false) && ($tagOpen_nested < $tagOpen_end))
 680			{
 681				$preTag .= substr($postTag, 0, ($tagOpen_nested + 1));
 682				$postTag = substr($postTag, ($tagOpen_nested + 1));
 683				$tagOpen_start = strpos($postTag, '<');
 684				continue;
 685			}
 686
 687			// Let's get some information about our tag and setup attribute pairs
 688			$tagOpen_nested = (strpos($fromTagOpen, '<') + $tagOpen_start + 1);
 689			$currentTag = substr($fromTagOpen, 0, $tagOpen_end);
 690			$tagLength = strlen($currentTag);
 691			$tagLeft = $currentTag;
 692			$attrSet = array();
 693			$currentSpace = strpos($tagLeft, ' ');
 694
 695			// Are we an open tag or a close tag?
 696			if (substr($currentTag, 0, 1) == '/')
 697			{
 698				// Close Tag
 699				$isCloseTag = true;
 700				list ($tagName) = explode(' ', $currentTag);
 701				$tagName = substr($tagName, 1);
 702			}
 703			else
 704			{
 705				// Open Tag
 706				$isCloseTag = false;
 707				list ($tagName) = explode(' ', $currentTag);
 708			}
 709
 710			/*
 711			 * Exclude all "non-regular" tagnames
 712			 * OR no tagname
 713			 * OR remove if xssauto is on and tag is blacklisted
 714			 */
 715			if ((!preg_match("/^[a-z][a-z0-9]*$/i", $tagName)) || (!$tagName) || ((in_array(strtolower($tagName), $this->tagBlacklist)) && ($this->xssAuto)))
 716			{
 717				$postTag = substr($postTag, ($tagLength + 2));
 718				$tagOpen_start = strpos($postTag, '<');
 719
 720				// Strip tag
 721				continue;
 722			}
 723
 724			/*
 725			 * Time to grab any attributes from the tag... need this section in
 726			 * case attributes have spaces in the values.
 727			 */
 728			while ($currentSpace !== false)
 729			{
 730				$attr = '';
 731				$fromSpace = substr($tagLeft, ($currentSpace + 1));
 732				$nextEqual = strpos($fromSpace, '=');
 733				$nextSpace = strpos($fromSpace, ' ');
 734				$openQuotes = strpos($fromSpace, '"');
 735				$closeQuotes = strpos(substr($fromSpace, ($openQuotes + 1)), '"') + $openQuotes + 1;
 736
 737				$startAtt = '';
 738				$startAttPosition = 0;
 739
 740				// Find position of equal and open quotes ignoring
 741				if (preg_match('#\s*=\s*\"#', $fromSpace, $matches, PREG_OFFSET_CAPTURE))
 742				{
 743					$startAtt = $matches[0][0];
 744					$startAttPosition = $matches[0][1];
 745					$closeQuotes = strpos(substr($fromSpace, ($startAttPosition + strlen($startAtt))), '"') + $startAttPosition + strlen($startAtt);
 746					$nextEqual = $startAttPosition + strpos($startAtt, '=');
 747					$openQuotes = $startAttPosition + strpos($startAtt, '"');
 748					$nextSpace = strpos(substr($fromSpace, $closeQuotes), ' ') + $closeQuotes;
 749				}
 750
 751				// Do we have an attribute to process? [check for equal sign]
 752				if ($fromSpace != '/' && (($nextEqual && $nextSpace && $nextSpace < $nextEqual) || !$nextEqual))
 753				{
 754					if (!$nextEqual)
 755					{
 756						$attribEnd = strpos($fromSpace, '/') - 1;
 757					}
 758					else
 759					{
 760						$attribEnd = $nextSpace - 1;
 761					}
 762					// If there is an ending, use this, if not, do not worry.
 763					if ($attribEnd > 0)
 764					{
 765						$fromSpace = substr($fromSpace, $attribEnd + 1);
 766					}
 767				}
 768
 769				if (strpos($fromSpace, '=') !== false)
 770				{
 771					// If the attribute value is wrapped in quotes we need to grab the substring from
 772					// the closing quote, otherwise grab until the next space.
 773					if (($openQuotes !== false) && (strpos(substr($fromSpace, ($openQuotes + 1)), '"') !== false))
 774					{
 775						$attr = substr($fromSpace, 0, ($closeQuotes + 1));
 776					}
 777					else
 778					{
 779						$attr = substr($fromSpace, 0, $nextSpace);
 780					}
 781				}
 782				// No more equal signs so add any extra text in the tag into the attribute array [eg. checked]
 783				else
 784				{
 785					if ($fromSpace != '/')
 786					{
 787						$attr = substr($fromSpace, 0, $nextSpace);
 788					}
 789				}
 790
 791				// Last Attribute Pair
 792				if (!$attr && $fromSpace != '/')
 793				{
 794					$attr = $fromSpace;
 795				}
 796
 797				// Add attribute pair to the attribute array
 798				$attrSet[] = $attr;
 799
 800				// Move search point and continue iteration
 801				$tagLeft = substr($fromSpace, strlen($attr));
 802				$currentSpace = strpos($tagLeft, ' ');
 803			}
 804
 805			// Is our tag in the user input array?
 806			$tagFound = in_array(strtolower($tagName), $this->tagsArray);
 807
 808			// If the tag is allowed let's append it to the output string.
 809			if ((!$tagFound && $this->tagsMethod) || ($tagFound && !$this->tagsMethod))
 810			{
 811				// Reconstruct tag with allowed attributes
 812				if (!$isCloseTag)
 813				{
 814					// Open or single tag
 815					$attrSet = $this->_cleanAttributes($attrSet);
 816					$preTag .= '<' . $tagName;
 817
 818					for ($i = 0, $count = count($attrSet); $i < $count; $i++)
 819					{
 820						$preTag .= ' ' . $attrSet[$i];
 821					}
 822
 823					// Reformat single tags to XHTML
 824					if (strpos($fromTagOpen, '</' . $tagName))
 825					{
 826						$preTag .= '>';
 827					}
 828					else
 829					{
 830						$preTag .= ' />';
 831					}
 832				}
 833				// Closing tag
 834				else
 835				{
 836					$preTag .= '</' . $tagName . '>';
 837				}
 838			}
 839
 840			// Find next tag's start and continue iteration
 841			$postTag = substr($postTag, ($tagLength + 2));
 842			$tagOpen_start = strpos($postTag, '<');
 843		}
 844
 845		// Append any code after the end of tags and return
 846		if ($postTag != '<')
 847		{
 848			$preTag .= $postTag;
 849		}
 850
 851		return $preTag;
 852	}
 853
 854	/**
 855	 * Internal method to strip a tag of certain attributes
 856	 *
 857	 * @param   array  $attrSet  Array of attribute pairs to filter
 858	 *
 859	 * @return  array  Filtered array of attribute pairs
 860	 *
 861	 * @since   11.1
 862	 */
 863	protected function _cleanAttributes($attrSet)
 864	{
 865		$newSet = array();
 866
 867		$count = count($attrSet);
 868
 869		// Iterate through attribute pairs
 870		for ($i = 0; $i < $count; $i++)
 871		{
 872			// Skip blank spaces
 873			if (!$attrSet[$i])
 874			{
 875				continue;
 876			}
 877
 878			// Split into name/value pairs
 879			$attrSubSet = explode('=', trim($attrSet[$i]), 2);
 880
 881			// Take the last attribute in case there is an attribute with no value
 882			$attrSubSet_0 = explode(' ', trim($attrSubSet[0]));
 883			$attrSubSet[0] = array_pop($attrSubSet_0);
 884
 885			// Remove all "non-regular" attribute names
 886			// AND blacklisted attributes
 887
 888			if ((!preg_match('/[a-z]*$/i', $attrSubSet[0]))
 889				|| (($this->xssAuto) && ((in_array(strtolower($attrSubSet[0]), $this->attrBlacklist))
 890				|| (substr($attrSubSet[0], 0, 2) == 'on'))))
 891			{
 892				continue;
 893			}
 894
 895			// XSS attribute value filtering
 896			if (isset($attrSubSet[1]))
 897			{
 898				// Trim leading and trailing spaces
 899				$attrSubSet[1] = trim($attrSubSet[1]);
 900
 901				// Strips unicode, hex, etc
 902				$attrSubSet[1] = str_replace('&#', '', $attrSubSet[1]);
 903
 904				// Strip normal newline within attr value
 905				$attrSubSet[1] = preg_replace('/[\n\r]/', '', $attrSubSet[1]);
 906
 907				// Strip double quotes
 908				$attrSubSet[1] = str_replace('"', '', $attrSubSet[1]);
 909
 910				// Convert single quotes from either side to doubles (Single quotes shouldn't be used to pad attr values)
 911				if ((substr($attrSubSet[1], 0, 1) == "'") && (substr($attrSubSet[1], (strlen($attrSubSet[1]) - 1), 1) == "'"))
 912				{
 913					$attrSubSet[1] = substr($attrSubSet[1], 1, (strlen($attrSubSet[1]) - 2));
 914				}
 915				// Strip slashes
 916				$attrSubSet[1] = stripslashes($attrSubSet[1]);
 917			}
 918			else
 919			{
 920				continue;
 921			}
 922
 923			// Autostrip script tags
 924			if (self::checkAttribute($attrSubSet))
 925			{
 926				continue;
 927			}
 928
 929			// Is our attribute in the user input array?
 930			$attrFound = in_array(strtolower($attrSubSet[0]), $this->attrArray);
 931
 932			// If the tag is allowed lets keep it
 933			if ((!$attrFound && $this->attrMethod) || ($attrFound && !$this->attrMethod))
 934			{
 935				// Does the attribute have a value?
 936				if (empty($attrSubSet[1]) === false)
 937				{
 938					$newSet[] = $attrSubSet[0] . '="' . $attrSubSet[1] . '"';
 939				}
 940				elseif ($attrSubSet[1] === "0")
 941				{
 942					// Special Case
 943					// Is the value 0?
 944					$newSet[] = $attrSubSet[0] . '="0"';
 945				}
 946				else
 947				{
 948					// Leave empty attributes alone
 949					$newSet[] = $attrSubSet[0] . '=""';
 950				}
 951			}
 952		}
 953
 954		return $newSet;
 955	}
 956
 957	/**
 958	 * Try to convert to plaintext
 959	 *
 960	 * @param   string  $source  The source string.
 961	 *
 962	 * @return  string  Plaintext string
 963	 *
 964	 * @since   11.1
 965	 */
 966	protected function _decode($source)
 967	{
 968		static $ttr;
 969
 970		if (!is_array($ttr))
 971		{
 972			// Entity decode
 973			$trans_tbl = get_html_translation_table(HTML_ENTITIES, ENT_COMPAT, 'ISO-8859-1');
 974
 975			foreach ($trans_tbl as $k => $v)
 976			{
 977				$ttr[$v] = utf8_encode($k);
 978			}
 979		}
 980
 981		$source = strtr($source, $ttr);
 982
 983		// Convert decimal
 984		$source = preg_replace_callback('/&#(\d+);/m', function($m)
 985		{
 986			return utf8_encode(chr($m[1]));
 987		}, $source
 988		);
 989
 990		// Convert hex
 991		$source = preg_replace_callback('/&#x([a-f0-9]+);/mi', function($m)
 992		{
 993			return utf8_encode(chr('0x' . $m[1]));
 994		}, $source
 995		);
 996
 997		return $source;
 998	}
 999
1000	/**
1001	 * Escape < > and " inside attribute values
1002	 *
1003	 * @param   string  $source  The source string.
1004	 *
1005	 * @return  string  Filtered string
1006	 *
1007	 * @since    11.1
1008	 */
1009	protected function _escapeAttributeValues($source)
1010	{
1011		$alreadyFiltered = '';
1012		$remainder = $source;
1013		$badChars = array('<', '"', '>');
1014		$escapedChars = array('&lt;', '&quot;', '&gt;');
1015
1016		// Process each portion based on presence of =" and "<space>, "/>, or ">
1017		// See if there are any more attributes to process
1018		while (preg_match('#<[^>]*?=\s*?(\"|\')#s', $remainder, $matches, PREG_OFFSET_CAPTURE))
1019		{
1020			// Get the portion before the attribute value
1021			$quotePosition = $matches[0][1];
1022			$nextBefore = $quotePosition + strlen($matches[0][0]);
1023
1024			// Figure out if we have a single or double quote and look for the matching closing quote
1025			// Closing quote should be "/>, ">, "<space>, or " at the end of the string
1026			$quote = substr($matches[0][0], -1);
1027			$pregMatch = ($quote == '"') ? '#(\"\s*/\s*>|\"\s*>|\"\s+|\"$)#' : "#(\'\s*/\s*>|\'\s*>|\'\s+|\'$)#";
1028
1029			// Get the portion after attribute value
1030			if (preg_match($pregMatch, substr($remainder, $nextBefore), $matches, PREG_OFFSET_CAPTURE))
1031			{
1032				// We have a closing quote
1033				$nextAfter = $nextBefore + $matches[0][1];
1034			}
1035			else
1036			{
1037				// No closing quote
1038				$nextAfter = strlen($remainder);
1039			}
1040
1041			// Get the actual attribute value
1042			$attributeValue = substr($remainder, $nextBefore, $nextAfter - $nextBefore);
1043
1044			// Escape bad chars
1045			$attributeValue = str_replace($badChars, $escapedChars, $attributeValue);
1046			$attributeValue = $this->_stripCSSExpressions($attributeValue);
1047			$alreadyFiltered .= substr($remainder, 0, $nextBefore) . $attributeValue . $quote;
1048			$remainder = substr($remainder, $nextAfter + 1);
1049		}
1050
1051		// At this point, we just have to return the $alreadyFiltered and the $remainder
1052		return $alreadyFiltered . $remainder;
1053	}
1054
1055	/**
1056	 * Remove CSS Expressions in the form of <property>:expression(...)
1057	 *
1058	 * @param   string  $source  The source string.
1059	 *
1060	 * @return  string  Filtered string
1061	 *
1062	 * @since   11.1
1063	 */
1064	protected function _stripCSSExpressions($source)
1065	{
1066		// Strip any comments out (in the form of /*...*/)
1067		$test = preg_replace('#\/\*.*\*\/#U', '', $source);
1068
1069		// Test for :expression
1070		if (!stripos($test, ':expression'))
1071		{
1072			// Not found, so we are done
1073			$return = $source;
1074		}
1075		else
1076		{
1077			// At this point, we have stripped out the comments and have found :expression
1078			// Test stripped string for :expression followed by a '('
1079			if (preg_match_all('#:expression\s*\(#', $test, $matches))
1080			{
1081				// If found, remove :expression
1082				$test = str_ireplace(':expression', '', $test);
1083				$return = $test;
1084			}
1085		}
1086
1087		return $return;
1088	}
1089}