_tagsAllowedObj = new TagsAllowed(); else $this->_tagsAllowedObj = $tagsAllowedObj; } /** * Основная функция для фильтрования * (оставляет разрешённые атрибуты у разрешённых тегов) * @param string $str необработанная строка * @return string обработанная строка */ public function parse($str) { if (empty ($str)) return ''; $this->_tagsAllowedArray = $this->_tagsAllowedObj->getAsArray(); /** * @TODO тут неправильно - может удалить прописанные теги */ if ($this->_tagsAllowedObj->haveOnlyDefaultParams()) return strip_tags($str); /** * создаём объект DomDocument с пользовательским содержимым, "грязный" */ $domObj = self::_convertString2DomDocument($str); /** * создаём пустой объект DomDocument, "чистый" */ $cleanDomObj = self::_convertString2DomDocument(''); /** * копируем в "чистый" DomDocument только разрешённые теги с разрешённым атрибутами * из "грязного" */ $this->_copyValidNodeAttributesRecursive($domObj->documentElement, $cleanDomObj, $cleanDomObj); $cleanDomObj = UrlsCleaner::cleanWithXPath($cleanDomObj, $this->_tagsAllowedObj); $cleanString = $cleanDomObj->saveHTML(); $cleanString = trim(substr($cleanString, stripos($cleanString, '') + 7)); return $cleanString; } /** * Очищает разрешённые теги от лишних атриутов путём * копирования разрешённых тегов с разрешённым атрибутами в "чистый" DomDocument * @param DomDocument $node "грязный" пользовательский элемент DomDocument * @param DomDocument $cleanDomObjNode "чистый" элемент DomDocument, * родительский для будущего соответствующего $node * @param DomDocument $cleanDomObj корень "чистого" DomDocument * @see http://ru2.php.net/manual/en/class.domdocument.php#domdocument.props.documentelement */ private function _copyValidNodeAttributesRecursive($node, $cleanDomObjNode, $cleanDomObj) { if ($node->nodeType == XML_TEXT_NODE) { /** * @see http://ru2.php.net/manual/en/domdocument.createtextnode.php */ $cleanNodeObjNewChild = $cleanDomObj->createTextNode($node->nodeValue); /** * @see http://ru2.php.net/manual/en/domnode.appendchild.php */ $cleanDomObjNode->appendChild($cleanNodeObjNewChild); } else if (array_key_exists($node->nodeName, $this->_tagsAllowedArray)) { /** * @see http://ru2.php.net/manual/en/domdocument.createelement.php */ $cleanNodeObjNewChild = $cleanDomObj->createElement($node->nodeName); if ($node->attributes) { $validAttributes = array(); foreach($node->attributes as $atribute) { $allowedAttrs = $this->_tagsAllowedArray[$node->nodeName]; /** * добавляем к атрибутам только разрешённые */ if(is_array($allowedAttrs) && in_array($atribute->name, $allowedAttrs)) $cleanNodeObjNewChild->setAttribute($atribute->name, $atribute->value); } } $cleanDomObjNode->appendChild($cleanNodeObjNewChild); /** * если тег валидный, то он становится родительским при дальнейшей рекурсии */ $cleanDomObjNode = $cleanNodeObjNewChild; } if ($node->nodeType != XML_TEXT_NODE && $node->childNodes) { foreach($node->childNodes as $child) { /** * продолжаем глубже */ $this->_copyValidNodeAttributesRecursive($child, $cleanDomObjNode, $cleanDomObj); } } } /** * Конвертирует строку в объект DomDocument с использованием кодировки * @param string $str * @return DomDocument */ private static function _convertString2DomDocument($str) { $text = ''.str_replace(array("\r\n", "\r"), "\n", $str); $dom = new DOMDocument('1.0', self::$_encoding); @$dom->loadHTML($text); return $dom; } } class UrlsCleaner { /** * все возможные места, в которых может встретиться урл * (ну чтоб не было "javascript:") * в виде тег => массив атрибутов */ private static $_allAttributesWithUrl = array ( 'a' => array('href'), 'area' => array('href'), 'link' => array('href'), 'img' => array('src', 'longdesc', 'usemap'), 'object' => array('classid', 'codebase', 'data', 'usemap'), 'applet' => array('codebase'), 'q' => array('cite'), 'blockquote' => array('cite'), 'form' => array('action'), 'input' => array('src', 'usemap'), 'frame' => array('longdesc', 'src'), 'input' => array('src'), 'iframe' => array('longdesc', 'src'), 'script' => array('src', 'for'), 'embed' => array('pluginpage', 'src'), /*'head' => array('profile'), 'body' => array('background'), 'base' => array('href'),*/ ); /** * Хранятся все разрешённые теги, в которых может быть url */ private static $_allowedTags4Urls = array(); /** * Хранятся XpathQuery для всех разрешённых тегов, в которых может быть url */ private static $_generatedXpathQuery = ''; private static $_urlAllowedSchemes = array('http', 'https', 'ftp', 'mailto'); /** * Основной метод. * Очищает все url'ы от использования неразрешённых протоколов, * а для ссылок на сторонние ресурсы проставляет target="_blank" * Использует XPath. * @param DOMdocument $domObj * @param TagsAllowed $tagsAllowedObj * @return DOMdocument */ public static function cleanWithXPath($domObj, $tagsAllowedObj) { $xPathObj = new DOMXPath($domObj); /** * для ссылок на сторонние ресурсы проставляем target="_blank" */ self::_setTargetBlankInTagA($xPathObj, $domObj); /** * готовим XPath->query-выражения с учётом разрешённых тегов, * в которых могут быть ссылки */ self::_setXPathQuery4UrlsInAllowedTags($tagsAllowedObj->getAsArray()); /** * удаляем неразрешённые протоколы из всех возможных ссылок */ if (!empty(self::$_generatedXpathQuery)) { foreach($xPathObj->query(self::$_generatedXpathQuery) as $entry) { foreach(self::$_allowedTags4Urls[$entry->tagName] as $attribute) { $url = strtolower(trim($entry->getAttribute($attribute))); if(strlen($url) === 0 || $url{0} == '#' || $url{0} == '/' || $url{0} == '.') $entry->setAttribute($attribute, $url); else { $urlVars = parse_url($url); /** * если протокол есть среди разрешённых, то оставляем, * нет - дописываем свой */ if(isset($urlVars['scheme']) && in_array($urlVars['scheme'], self::$_urlAllowedSchemes)) $entry->setAttribute($attribute, $url); else $entry->setAttribute($attribute, 'http://'.$url); } } } } return $domObj; } /** * Устанавливаем target="_blank" для ссылок не с этого сервера * @param DOMXPath $xPathObj * @return DOMXPath */ private static function _setTargetBlankInTagA($xPathObj, $domObj) { $host = self::_getLocalUrlPrefix(); $aHrefs = $xPathObj->query('//a[@href]'); foreach($aHrefs as $aHref) { if(strpos($aHref->getAttribute('href'), $host) === false) { if(($target = $aHref->getAttributeNode('target'))) $target->value = '_blank'; else { $target = $domObj->createAttribute('target'); $target->value = '_blank'; $aHref->appendChild($target); } } } return $xPathObj; } /** * Возвращает префикс местного сервера * (для простановки target="_blank") * @link _setTargetBlankInTagA() * @return string */ private static function _getLocalUrlPrefix() { return 'http://localhost/'; } /** * Устанавливаем выражение для XPath->query для разрешённых тегов, * в которых могут быть урлы, а также все атрибуты с тегами в отдельный массив * @param array $tagsAllowedArray массив разрешённых тегов * Устанавливает self::$_allowedTags4Urls и self::$_generatedXpathQuery * поэтому не через возврат значения сделал * @return void */ private static function _setXPathQuery4UrlsInAllowedTags($tagsAllowedArray) { self::$_allowedTags4Urls = $queries = array(); self::$_generatedXpathQuery = ''; foreach(self::$_allAttributesWithUrl as $tag => $attributes) { if(!isset($tagsAllowedArray[$tag]) || count($attributes = array_intersect($attributes, $tagsAllowedArray[$tag])) === 0) continue; self::$_allowedTags4Urls[$tag] = $attributes; $queries[] = '//'.$tag.'[@'.implode(' or @', $attributes).']'; } self::$_generatedXpathQuery = implode(' | ', $queries); } } /** * Здесь хранятся разрешённые теги */ class TagsAllowed { //оставил - для потомков)). на одном этапе понадобилось, теперь не нужен. private static $_defaultParams = array ( /*'html' => array(), 'head' => array(), 'meta' => array('http-equiv', 'content', 'charset'), 'body' => array()*/ //'p' => array() ); /** * Массив всех параметров в виде тег => простой массив параметров или 1 * @var array */ private $_params = array (); /*( 'a' => array('href'), 'area' => array('href'), 'link' => array('href'), 'img' => array('src', 'longdesc', 'usemap'), 'object' => array('classid', 'codebase', 'data', 'usemap'), 'applet' => array('codebase'), 'q' => array('cite'), 'blockquote' => array('cite'), 'form' => array('action'), 'input' => array('src', 'usemap'), 'frame' => array('longdesc', 'src'), 'input' => array('src'), 'iframe' => array('longdesc', 'src'), 'script' => array('src', 'for'), 'embed' => array('pluginpage', 'src') );*/ /** * Массив всех параметров в виде тег => простой массив параметров * @example new self(array('a' => array('href'), 'input' => array('src', 'type'))) * @param array $params */ public function __construct($params = array()) { if (self::_checkParamsStructure($params)) { /** * это для того, чтобы работать с валидным html-кодом, * который создаёт DOMDocument::loadHTML */ $this->_params = array_merge(self::$_defaultParams, $params); } else throw new TagsAllowedExceptionWrongParams(); } /** * Factory-метод для статей * @return TagsAllowed */ public static function getForArticlesFactory() { $params = array ( 'a' => array('href', 'target'), 'b' => 1, 'strong' => 1, 'i' => 1, 'u' => 1, 'img' => array('src'), 'quote' => 1 ); return new self($params); } /** * Factory-метод для комментариев * @return TagsAllowed */ public static function getForCommentsFactory() { $params = array ( 'a' => array('href', 'target'), 'b' => 1, 'strong' => 1, 'i' => 1, 'quote' => 1 ); return new self($params); } /** * Factory-метод для вырезания всех тегов * @return TagsAllowed */ public static function getForStripAll() { return new self(array()); } /** * Проверяет, правильная ли структура переданного массива параметров фильтра * @TODO нужна более серьёзная проверка * @param array $params * @return bool */ private static function _checkParamsStructure($params) { if (is_array($params)) return true; return false; } public function getAsArray() { return $this->_params; } public function haveOnlyDefaultParams() { if ($this->_params == self::$_defaultParams) return true; return false; } } class TagsAllowedExceptionWrongParams extends Exception {} ?>