From e7c53af6fa3ade7540f03ebc8d701bc5c70bf499 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Sun, 31 Dec 2017 20:58:09 -0500 Subject: [PATCH] Add Module\Oembed and Content\OEmbed --- include/bbcode.php | 7 +- src/Content/OEmbed.php | 352 +++++++++++++++++++++++++++++++++++++++++ src/Module/Oembed.php | 53 +++++++ src/ParseUrl.php | 4 +- src/Protocol/DFRN.php | 3 +- 5 files changed, 413 insertions(+), 6 deletions(-) create mode 100644 src/Content/OEmbed.php create mode 100644 src/Module/Oembed.php diff --git a/include/bbcode.php b/include/bbcode.php index be59c18072..e672001937 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -2,6 +2,7 @@ use Friendica\App; use Friendica\Content\Smilies; +use Friendica\Content\OEmbed; use Friendica\Core\Cache; use Friendica\Core\System; use Friendica\Core\Config; @@ -232,7 +233,7 @@ function tryoembed($match) { $url = str_replace(array("http://www.youtube.com/", "http://player.vimeo.com/"), array("https://www.youtube.com/", "https://player.vimeo.com/"), $url); - $o = oembed_fetch_url($url); + $o = OEmbed::fetchURL($url); if (!is_object($o)) { return $match[0]; @@ -246,7 +247,7 @@ function tryoembed($match) { return $match[0]; } - $html = oembed_format_object($o); + $html = OEmbed::formatObject($o); return $html; } @@ -1263,7 +1264,7 @@ function bbcode($Text, $preserve_nl = false, $tryoembed = true, $simplehtml = fa // $Text = preg_replace("/\[youtube\](.*?)\[\/youtube\]/", '', $Text); // oembed tag - $Text = oembed_bbcode2html($Text); + $Text = OEmbed::BBCode2HTML($Text); // Avoid triple linefeeds through oembed $Text = str_replace("


", "

", $Text); diff --git a/src/Content/OEmbed.php b/src/Content/OEmbed.php new file mode 100644 index 0000000000..db2c130eaf --- /dev/null +++ b/src/Content/OEmbed.php @@ -0,0 +1,352 @@ + normalise_link($embedurl)); + $r = dba::select('oembed', array('content'), $condition, array('limit' => 1)); + + if (DBM::is_result($r)) { + $txt = $r["content"]; + } else { + $txt = Cache::get($a->videowidth . $embedurl); + } + // These media files should now be caught in bbcode.php + // left here as a fallback in case this is called from another source + + $noexts = array("mp3", "mp4", "ogg", "ogv", "oga", "ogm", "webm"); + $ext = pathinfo(strtolower($embedurl), PATHINFO_EXTENSION); + + + if (is_null($txt)) { + $txt = ""; + + if (!in_array($ext, $noexts)) { + // try oembed autodiscovery + $redirects = 0; + $html_text = fetch_url($embedurl, false, $redirects, 15, "text/*"); + if ($html_text) { + $dom = @DOMDocument::loadHTML($html_text); + if ($dom) { + $xpath = new DOMXPath($dom); + $entries = $xpath->query("//link[@type='application/json+oembed']"); + foreach ($entries as $e) { + $href = $e->getAttributeNode("href")->nodeValue; + $txt = fetch_url($href . '&maxwidth=' . $a->videowidth); + break; + } + $entries = $xpath->query("//link[@type='text/json+oembed']"); + foreach ($entries as $e) { + $href = $e->getAttributeNode("href")->nodeValue; + $txt = fetch_url($href . '&maxwidth=' . $a->videowidth); + break; + } + } + } + } + + $txt = trim($txt); + + if ($txt[0] != "{") { + $txt = '{"type":"error"}'; + } else { //save in cache + $j = json_decode($txt); + if ($j->type != "error") { + dba::insert('oembed', array('url' => normalise_link($embedurl), + 'content' => $txt, 'created' => datetime_convert()), true); + } + + Cache::set($a->videowidth . $embedurl, $txt, CACHE_DAY); + } + } + + $j = json_decode($txt); + + if (!is_object($j)) { + return false; + } + + // Always embed the SSL version + if (isset($j->html)) { + $j->html = str_replace(array("http://www.youtube.com/", "http://player.vimeo.com/"), array("https://www.youtube.com/", "https://player.vimeo.com/"), $j->html); + } + + $j->embedurl = $embedurl; + + // If fetching information doesn't work, then improve via internal functions + if (($j->type == "error") || ($no_rich_type && ($j->type == "rich"))) { + $data = ParseUrl::getSiteinfoCached($embedurl, true, false); + $j->type = $data["type"]; + + if ($j->type == "photo") { + $j->url = $data["url"]; + //$j->width = $data["images"][0]["width"]; + //$j->height = $data["images"][0]["height"]; + } + + if (isset($data["title"])) { + $j->title = $data["title"]; + } + + if (isset($data["text"])) { + $j->description = $data["text"]; + } + + if (is_array($data["images"])) { + $j->thumbnail_url = $data["images"][0]["src"]; + $j->thumbnail_width = $data["images"][0]["width"]; + $j->thumbnail_height = $data["images"][0]["height"]; + } + } + + call_hooks('oembed_fetch_url', $embedurl, $j); + + return $j; + } + + public static function formatObject($j) + { + $embedurl = $j->embedurl; + $jhtml = OEmbed::iframe($j->embedurl, (isset($j->width) ? $j->width : null), (isset($j->height) ? $j->height : null)); + $ret = ""; + switch ($j->type) { + case "video": + if (isset($j->thumbnail_url)) { + $tw = (isset($j->thumbnail_width) && intval($j->thumbnail_width)) ? $j->thumbnail_width : 200; + $th = (isset($j->thumbnail_height) && intval($j->thumbnail_height)) ? $j->thumbnail_height : 180; + // make sure we don't attempt divide by zero, fallback is a 1:1 ratio + $tr = (($th) ? $tw / $th : 1); + + $th = 120; + $tw = $th * $tr; + $tpl = get_markup_template('oembed_video.tpl'); + $ret.=replace_macros($tpl, array( + '$baseurl' => System::baseUrl(), + '$embedurl' => $embedurl, + '$escapedhtml' => base64_encode($jhtml), + '$tw' => $tw, + '$th' => $th, + '$turl' => $j->thumbnail_url, + )); + } else { + $ret = $jhtml; + } + //$ret.="
"; + break; + case "photo": + $ret.= ""; + break; + case "link": + break; + case "rich": + // not so safe.. + if (!Config::get("system", "no_oembed_rich_content")) { + $ret.= proxy_parse_html($jhtml); + } + break; + } + + // add link to source if not present in "rich" type + if ($j->type != 'rich' || !strpos($j->html, $embedurl)) { + $ret .= "

"; + if (isset($j->title)) { + if (isset($j->provider_name)) { + $ret .= $j->provider_name . ": "; + } + + $embedlink = (isset($j->title)) ? $j->title : $embedurl; + $ret .= "$embedlink"; + if (isset($j->author_name)) { + $ret.=" (" . $j->author_name . ")"; + } + } elseif (isset($j->provider_name) || isset($j->author_name)) { + $embedlink = ""; + if (isset($j->provider_name)) { + $embedlink .= $j->provider_name; + } + + if (isset($j->author_name)) { + if ($embedlink != "") { + $embedlink .= ": "; + } + + $embedlink .= $j->author_name; + } + if (trim($embedlink) == "") { + $embedlink = $embedurl; + } + + $ret .= "$embedlink"; + } + //if (isset($j->author_name)) $ret.=" by ".$j->author_name; + //if (isset($j->provider_name)) $ret.=" on ".$j->provider_name; + $ret .= "

"; + } else { + // add for html2bbcode conversion + $ret .= "$embedurl"; + } + $ret.="
"; + $ret = str_replace("\n", "", $ret); + return mb_convert_encoding($ret, 'HTML-ENTITIES', mb_detect_encoding($ret)); + } + + public static function BBCode2HTML($text) + { + $stopoembed = Config::get("system", "no_oembed"); + if ($stopoembed == true) { + return preg_replace("/\[embed\](.+?)\[\/embed\]/is", "" . t('Embedding disabled') . " : $1", $text); + } + return preg_replace_callback("/\[embed\](.+?)\[\/embed\]/is", ['self', 'replaceCallback'], $text); + } + + /** + * Find .... + * and replace it with [embed]url[/embed] + */ + public static function HTML2BBCode($text) + { + // start parser only if 'oembed' is in text + if (strpos($text, "oembed")) { + + // convert non ascii chars to html entities + $html_text = mb_convert_encoding($text, 'HTML-ENTITIES', mb_detect_encoding($text)); + + // If it doesn't parse at all, just return the text. + $dom = @DOMDocument::loadHTML($html_text); + if (!$dom) { + return $text; + } + $xpath = new DOMXPath($dom); + + $xattr = OEmbed::buildXPath("class", "oembed"); + $entries = $xpath->query("//span[$xattr]"); + + $xattr = "@rel='oembed'"; //oe_build_xpath("rel","oembed"); + foreach ($entries as $e) { + $href = $xpath->evaluate("a[$xattr]/@href", $e)->item(0)->nodeValue; + if (!is_null($href)) { + $e->parentNode->replaceChild(new DOMText("[embed]" . $href . "[/embed]"), $e); + } + } + return OEmbed::getInnerHTML($dom->getElementsByTagName("body")->item(0)); + } else { + return $text; + } + } + + /** + * @brief Generates the iframe HTML for an oembed attachment. + * + * Width and height are given by the remote, and are regularly too small for + * the generated iframe. + * + * The width is entirely discarded for the actual width of the post, while fixed + * height is used as a starting point before the inevitable resizing. + * + * Since the iframe is automatically resized on load, there are no need for ugly + * and impractical scrollbars. + * + * @param string $src Original remote URL to embed + * @param string $width + * @param string $height + * @return string formatted HTML + * + * @see oembed_format_object() + */ + private static function iframe($src, $width, $height) + { + $a = get_app(); + + if (!$height || strstr($height, '%')) { + $height = '200'; + } + $width = '100%'; + + $s = System::baseUrl() . '/oembed/' . base64url_encode($src); + return ''; + } + + /** + * Generates an XPath query to select elements whose provided attribute contains + * the provided value in a space-separated list. + * + * @brief Generates attribute search XPath string + * + * @param string $attr Name of the attribute to seach + * @param string $value Value to search in a space-separated list + * @return string + */ + private static function buildXPath($attr, $value) + { + // https://www.westhoffswelt.de/blog/2009/6/9/select-html-elements-with-more-than-one-css-class-using-xpath + return "contains( normalize-space( @$attr ), ' $value ' ) or substring( normalize-space( @$attr ), 1, string-length( '$value' ) + 1 ) = '$value ' or substring( normalize-space( @$attr ), string-length( @$attr ) - string-length( '$value' ) ) = ' $value' or @$attr = '$value'"; + } + + /** + * Returns the inner XML string of a provided DOMNode + * + * @brief Returns the inner XML string of a provided DOMNode + * + * @param DOMNode $node + * @return string + */ + private static function getInnerHTML(DOMNode $node) + { + $innerHTML = ''; + $children = $node->childNodes; + foreach ($children as $child) { + $innerHTML .= $child->ownerDocument->saveXML($child); + } + return $innerHTML; + } +} diff --git a/src/Module/Oembed.php b/src/Module/Oembed.php new file mode 100644 index 0000000000..f30fb861bc --- /dev/null +++ b/src/Module/Oembed.php @@ -0,0 +1,53 @@ + + */ +class Oembed extends BaseModule +{ + public static function content() + { + $a = self::getApp(); + + // Unused form: /oembed/b2h?url=... + if ($a->argv[1] == 'b2h') { + $url = array("", trim(hex2bin($_REQUEST['url']))); + echo Content\OEmbed::replaceCallback($url); + killme(); + } + + // Unused form: /oembed/h2b?text=... + if ($a->argv[1] == 'h2b') { + $text = trim(hex2bin($_REQUEST['text'])); + echo Content\OEmbed::HTML2BBCode($text); + killme(); + } + + if ($a->argc == 2) { + echo ''; + $url = base64url_decode($a->argv[1]); + $j = Content\OEmbed::fetchURL($url); + + // workaround for media.ccc.de (and any other endpoint that return size 0) + if (substr($j->html, 0, 7) == "html, 'width="0"')) { + $j->html = '' . $j->html; + $j->html = str_replace('width="0"', '', $j->html); + $j->html = str_replace('height="0"', '', $j->html); + } + echo $j->html; + echo ''; + } + killme(); + } +} diff --git a/src/ParseUrl.php b/src/ParseUrl.php index 9e46281ec9..6252e0b68a 100644 --- a/src/ParseUrl.php +++ b/src/ParseUrl.php @@ -5,7 +5,7 @@ */ namespace Friendica; -use Friendica\Core\Config; +use Friendica\Content\OEmbed; use Friendica\Object\Image; use Friendica\Util\XML; @@ -164,7 +164,7 @@ class ParseUrl $body = $data["body"]; if ($do_oembed) { - $oembed_data = oembed_fetch_url($url); + $oembed_data = OEmbed::fetchURL($url); if (!in_array($oembed_data->type, array("error", "rich", ""))) { $siteinfo["type"] = $oembed_data->type; diff --git a/src/Protocol/DFRN.php b/src/Protocol/DFRN.php index 5e9c91645b..1ec0c792e0 100644 --- a/src/Protocol/DFRN.php +++ b/src/Protocol/DFRN.php @@ -8,6 +8,7 @@ */ namespace Friendica\Protocol; +use Friendica\Content\OEmbed; use Friendica\Core\Config; use Friendica\Core\System; use Friendica\Core\Worker; @@ -2502,7 +2503,7 @@ class DFRN $item['body'] = html2bb_video($item['body']); - $item['body'] = oembed_html2bbcode($item['body']); + $item['body'] = OEmbed::HTML2BBCode($item['body']); $config = \HTMLPurifier_Config::createDefault(); $config->set('Cache.DefinitionImpl', null);