X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FContent%2FText%2FHTML.php;h=ebd74121b11552414cca0473c9fd305867326614;hb=200bdb55baf58ec149dfecb445e65f31d0a7ead2;hp=e1caca48ae04c399ae7809052fa3d775dfbb0dad;hpb=6f290607de7f10cea7429aacd0b394fd3f4c4e69;p=friendica.git diff --git a/src/Content/Text/HTML.php b/src/Content/Text/HTML.php index e1caca48ae..ebd74121b1 100644 --- a/src/Content/Text/HTML.php +++ b/src/Content/Text/HTML.php @@ -1,6 +1,6 @@ childNodes as $key => $child) { /* Remove empty text nodes at the start or at the end of the children list */ - if ($key > 0 && $key < $node->childNodes->length - 1 || $child->nodeName != '#text' || trim($child->nodeValue)) { + if ($key > 0 && $key < $node->childNodes->length - 1 || $child->nodeName != '#text' || trim($child->nodeValue) !== '') { $newNode = $child->cloneNode(true); $node->parentNode->insertBefore($newNode, $node); } @@ -141,8 +143,16 @@ class HTML * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function toBBCode($message, $basepath = '') + public static function toBBCode(string $message, string $basepath = ''): string { + /* + * Check if message is empty to prevent a lot of code below from being executed + * for just an empty message. + */ + if ($message === '') { + return ''; + } + DI::profiler()->startRecording('rendering'); $message = str_replace("\r", "", $message); @@ -271,8 +281,8 @@ class HTML self::tagToBBCode($doc, 'div', [], "\r", "\r"); self::tagToBBCode($doc, 'p', [], "\n", "\n"); - self::tagToBBCode($doc, 'ul', [], "[list]", "[/list]"); - self::tagToBBCode($doc, 'ol', [], "[list=1]", "[/list]"); + self::tagToBBCode($doc, 'ul', [], "[ul]", "[/ul]"); + self::tagToBBCode($doc, 'ol', [], "[ol]", "[/ol]"); self::tagToBBCode($doc, 'li', [], "[*]", ""); self::tagToBBCode($doc, 'hr', [], "[hr]", ""); @@ -409,7 +419,7 @@ class HTML * * @return string The expanded URL */ - private static function qualifyURLsSub($matches, $basepath) + private static function qualifyURLsSub(array $matches, string $basepath): string { $base = parse_url($basepath); unset($base['query']); @@ -436,7 +446,7 @@ class HTML * * @return string Body with expanded URLs */ - private static function qualifyURLs($body, $basepath) + private static function qualifyURLs(string $body, string $basepath): string { $URLSearchString = "^\[\]"; @@ -462,7 +472,7 @@ class HTML return $body; } - private static function breakLines($line, $level, $wraplength = 75) + private static function breakLines(string $line, int $level, int $wraplength = 75): string { if ($wraplength == 0) { $wraplength = 2000000; @@ -503,7 +513,7 @@ class HTML return implode("\n", $newlines); } - private static function quoteLevel($message, $wraplength = 75) + private static function quoteLevel(string $message, int $wraplength = 75): string { $lines = explode("\n", $message); @@ -539,7 +549,7 @@ class HTML return implode("\n", $newlines); } - private static function collectURLs($message) + private static function collectURLs(string $message): array { $pattern = '/(.*?)<\/a>/is'; preg_match_all($pattern, $message, $result, PREG_SET_ORDER); @@ -585,7 +595,7 @@ class HTML * @param bool $compact True: Completely strips image tags; False: Keeps image URLs * @return string */ - public static function toPlaintext(string $html, $wraplength = 75, $compact = false) + public static function toPlaintext(string $html, int $wraplength = 75, bool $compact = false): string { DI::profiler()->startRecording('rendering'); $message = str_replace("\r", "", $html); @@ -705,7 +715,7 @@ class HTML * @param string $html * @return string */ - public static function toMarkdown($html) + public static function toMarkdown(string $html): string { DI::profiler()->startRecording('rendering'); $converter = new HtmlConverter(['hard_break' => true]); @@ -721,29 +731,29 @@ class HTML * @param string $s * @return string */ - public static function toBBCodeVideo($s) + public static function toBBCodeVideo(string $s): string { $s = preg_replace( '#]+>(.*?)https?://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+)(.*?)#ism', '[youtube]$2[/youtube]', $s ); - + $s = preg_replace( '#](.*?)https?://www.youtube.com/embed/([A-Za-z0-9\-_=]+)(.*?)#ism', '[youtube]$2[/youtube]', $s ); - + $s = preg_replace( '#](.*?)https?://player.vimeo.com/video/([0-9]+)(.*?)#ism', '[vimeo]$2[/vimeo]', $s ); - + return $s; } - + /** * transform link href and img src from relative to absolute * @@ -751,56 +761,46 @@ class HTML * @param string $base base url * @return string */ - public static function relToAbs($text, $base) + public static function relToAbs(string $text, string $base): string { if (empty($base)) { return $text; } - + $base = rtrim($base, '/'); - + $base2 = $base . "/"; - + // Replace links $pattern = "/]*) href=\"(?!http|https|\/)([^\"]*)\"/"; $replace = "'; - } - /** * Loader for infinite scrolling * * @return string html for loader * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function scrollLoader() + public static function scrollLoader(): string { $tpl = Renderer::getMarkupTemplate("scroll_loader.tpl"); return Renderer::replaceMacros($tpl, [ @@ -829,7 +829,7 @@ class HTML * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function micropro($contact, $redirect = false, $class = '', $textmode = false) + public static function micropro(array $contact, bool $redirect = false, string $class = '', bool $textmode = false): string { // Use the contact URL if no address is available if (empty($contact['addr'])) { @@ -842,7 +842,7 @@ class HTML if ($redirect) { $url = Contact::magicLinkByContact($contact); - if (strpos($url, 'redir/') === 0) { + if (strpos($url, 'contact/redir/') === 0) { $sparkle = ' sparkle'; } } @@ -869,13 +869,12 @@ class HTML * * @param string $s Search query. * @param string $id HTML id - * @param string $url Search url. * @param bool $aside Display the search widgit aside. * * @return string Formatted HTML. * @throws \Exception */ - public static function search($s, $id = 'search-box', $aside = true) + public static function search(string $s, string $id = 'search-box', bool $aside = true): string { $mode = 'text'; @@ -910,19 +909,6 @@ class HTML return Renderer::replaceMacros(Renderer::getMarkupTemplate('searchbox.tpl'), $values); } - /** - * Replace naked text hyperlink with HTML formatted hyperlink - * - * @param string $s - * @return string - */ - public static function toLink($s) - { - $s = preg_replace("/(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\'\%\$\!\+]*)/", ' $1', $s); - $s = preg_replace("/\<(.*?)(src|href)=(.*?)\&\;(.*?)\>/ism", '<$1$2=$3&$4>', $s); - return $s; - } - /** * Given a HTML text and a set of filtering reasons, adds a content hiding header with the provided reasons * @@ -933,7 +919,7 @@ class HTML * @return string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function applyContentFilter($html, array $reasons) + public static function applyContentFilter(string $html, array $reasons): string { if (count($reasons)) { $tpl = Renderer::getMarkupTemplate('wall/content_filter.tpl'); @@ -953,7 +939,7 @@ class HTML * @param string $s * @return string */ - public static function unamp($s) + public static function unamp(string $s): string { return str_replace('&', '&', $s); } @@ -994,6 +980,7 @@ class HTML $config->set('Attr.AllowedRel', [ 'noreferrer' => true, 'noopener' => true, + 'tag' => true, ]); $config->set('Attr.AllowedFrameTargets', [ '_blank' => true, @@ -1022,4 +1009,77 @@ class HTML return $text; } + + /** + * XPath arbitrary string quoting + * + * @see https://stackoverflow.com/a/45228168 + * @param string $value + * @return string + */ + public static function xpathQuote(string $value): string + { + if (false === strpos($value, '"')) { + return '"' . $value . '"'; + } + + if (false === strpos($value, "'")) { + return "'" . $value . "'"; + } + + // if the value contains both single and double quotes, construct an + // expression that concatenates all non-double-quote substrings with + // the quotes, e.g.: + // + // concat("'foo'", '"', "bar") + return 'concat(' . implode(', \'"\', ', array_map([self::class, 'xpathQuote'], explode('"', $value))) . ')'; + } + + /** + * Checks if the provided URL is present in the DOM document in an element with the rel="me" attribute + * + * XHTML Friends Network http://gmpg.org/xfn/ + * + * @param DOMDocument $doc + * @param UriInterface $meUrl + * @return bool + */ + public static function checkRelMeLink(DOMDocument $doc, UriInterface $meUrl): bool + { + $xpath = new \DOMXpath($doc); + + // This expression checks that "me" is among the space-delimited values of the "rel" attribute. + // And that the href attribute contains exactly the provided URL + $expression = "//*[contains(concat(' ', normalize-space(@rel), ' '), ' me ')][@href = " . self::xpathQuote($meUrl) . "]"; + + $result = $xpath->query($expression); + + return $result !== false && $result->length > 0; + } + + /** + * @param DOMDocument $doc + * @return string|null Lowercase charset + */ + public static function extractCharset(DOMDocument $doc): ?string + { + $xpath = new DOMXPath($doc); + + $expression = "string(//meta[@charset]/@charset)"; + if ($charset = $xpath->evaluate($expression)) { + return strtolower($charset); + } + + try { + // This expression looks for a meta tag with the http-equiv attribute set to "content-type" ignoring case + // whose content attribute contains a "charset" string and returns its value + $expression = "string(//meta[@http-equiv][translate(@http-equiv, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'content-type'][contains(translate(@content, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'charset')]/@content)"; + $mediaType = MediaType::fromContentType($xpath->evaluate($expression)); + if (isset($mediaType->parameters['charset'])) { + return strtolower($mediaType->parameters['charset']); + } + } catch(\InvalidArgumentException $e) {} + + return null; + } }