* @author Jan Schneider * @author Chuck Hagenbuch * @category Horde * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Text_Filter */ class Horde_Text_Filter_Linkurls extends Horde_Text_Filter_Base { /** * Link-finding regex */ public static $regex = ''; /** * Filter parameters. * * @var array */ protected $_params = array( 'callback' => null, 'class' => '', 'encode' => false, 'nofollow' => false, 'target' => '_blank', 'secretKey' => null ); /** * Return the regex used to search for links. * * @return string The regex string. */ public static function getRegex() { if (!self::$regex) { self::initializeRegex(); } return self::$regex; } /** * Initialize the regex for this instance. */ public static function initializeRegex() { self::$regex = <<\[\]]+ # Run of non-space, non-()<> (??«»“”‘’]{2}) # that is not followed by two or more # punct chars that indicate end-of-url | # - or - \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels )+ (?: # End with: \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels | # - or - [^\s`!()\[\]{};:\'".,<>?«»“”‘’] # not a space or one of these punct # chars ) ) END_OF_REGEX; } /** * Returns a hash with replace patterns. * * @return array Patterns hash. */ public function getPatterns() { return array( 'regexp_callback' => array('@' . self::getRegex() . '@' => array($this, 'callback')) ); } /** */ public function callback($match) { $href = $orig_href = $match[0]; if (strpos($match[2], ':') === false) { $href = 'http://' . $href; } if ($this->_params['callback']) { $href = call_user_func($this->_params['callback'], $href); } $href = htmlspecialchars($href); $class = $this->_params['class']; if (!empty($class)) { $class = ' class="' . $class . '"'; } $target = $this->_params['target']; if (!empty($target)) { $target = ' target="' . $target . '"'; } $decoded = $orig_href; try { $host = $this->_parseurl($orig_href, PHP_URL_HOST); if ($host !== null && strlen($host)) { $decoded = substr_replace( $orig_href, Horde_Idna::decode($host), strpos($orig_href, $host), strlen($host) ); } } catch (Horde_Idna_Exception $e) { } catch (InvalidArgumentException $e) {} $replacement = '_params['nofollow'] ? ' rel="nofollow"' : '') . $target . $class . '>' . htmlspecialchars($decoded) . ''; if (!empty($this->_params['noprefetch'])) { $replacement = '' . $replacement . ''; } $secret = new Horde_Secret(); if (empty($this->_params['secretKey'])) { $this->_params['secretKey'] = $secret->setKey(); } if ($this->_params['encode']) { $replacement = chr(0) . chr(0) . chr(0) . base64_encode($secret->write($this->_params['secretKey'], $replacement)) . chr(0) . chr(0) . chr(0); } return $replacement; } /** * "Decodes" the text formerly encoded by using the "encode" parameter. * * @param string $text An encoded text. * @param string $key An optional key to use with Horde_Secret encryption. * If omitted a key will be fetched from a Horde_Secret * instance. * * @return string The decoded text. */ public static function decode($text, $key = null) { $secret = new Horde_Secret(); if (empty($key)) { $key = $secret->getKey(); } return preg_replace_callback( '/\00\00\00([\w=+\/]*)\00\00\00/', function($hex) use ($secret, $key) { return $secret->read($key, base64_decode($hex[1])); }, $text); } /** * Handle multi-byte data since parse_url is not multibyte safe on all * systems. Adapted from php.net/parse_url comments. * * See https://bugs.php.net/bug.php?id=52923 for description of * parse_url issues. * * @param string $url The url to parse. * @param int $component * * @return mixed The parsed url. * @throws InvalidArgumentException */ protected function _parseurl(string $url, int $component) { $enc_url = preg_replace_callback( '%[^:/@?&=#]+%usD', function ($matches) { return urlencode($matches[0]); }, $url ); $parts = @parse_url($enc_url, $component); if ($parts === false) { throw new InvalidArgumentException('Malformed URL: ' . $url); } return $parts !== null ? urldecode($parts) : null; } }