From 4f323efdf7abc5452152a87241e320aca20ce486 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 12 Nov 2010 17:41:35 -0800 Subject: [PATCH] Encapsulate the oEmbed -> oohembed fallback into oEmbedHelper class. Also added a chance to whitelist sites that don't show discovery info but do have oEmbed API endpoints, and to provide alternate APIs for some common services. Newly supported: - TwitPic: added a local function using TwitPic's API, since the oohembed implementation for TwitPic produced invalid output which Services_oEmbed rejects. (bug filed upstream) Tweaked... - Flickr: works, now using whitelist to use their endpoint directly instead of going through oohembed - Youtube: worked around a bug in Services_oEmbed which broke the direct use of API discovery info, so we don't have to use oohembed. Not currently working... - YFrog: whitelisting their endpoint directly as the oohembed output is broken, but this doesn't appear to work currently as I think things are confused by YFrog's servers giving a '204 No Content' response on our HEAD checks on the original link. --- classes/File_oembed.php | 20 +--- lib/oembedhelper.php | 246 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 15 deletions(-) create mode 100644 lib/oembedhelper.php diff --git a/classes/File_oembed.php b/classes/File_oembed.php index bcb2f7bacc..b7bf3a5dae 100644 --- a/classes/File_oembed.php +++ b/classes/File_oembed.php @@ -59,25 +59,15 @@ class File_oembed extends Memcached_DataObject } function _getOembed($url) { - require_once INSTALLDIR.'/extlib/Services/oEmbed.php'; $parameters = array( 'maxwidth' => common_config('attachments', 'thumb_width'), 'maxheight' => common_config('attachments', 'thumb_height'), ); - try{ - $oEmbed = new Services_oEmbed($url); - $object = $oEmbed->getObject($parameters); - return $object; - }catch(Exception $e){ - try{ - $oEmbed = new Services_oEmbed($url, array( - Services_oEmbed::OPTION_API => common_config('oohembed', 'endpoint') - )); - $object = $oEmbed->getObject($parameters); - return $object; - }catch(Exception $ex){ - return false; - } + try { + return oEmbedHelper::getObject($url, $parameters); + } catch (Exception $e) { + common_log(LOG_ERR, "Error during oembed lookup for $url - " . $e->getMessage()); + return false; } } diff --git a/lib/oembedhelper.php b/lib/oembedhelper.php new file mode 100644 index 0000000000..ef2b59214c --- /dev/null +++ b/lib/oembedhelper.php @@ -0,0 +1,246 @@ +. + */ + +if (!defined('STATUSNET')) { + exit(1); +} +require_once INSTALLDIR.'/extlib/Services/oEmbed.php'; + + +/** + * Utility class to wrap Services_oEmbed: + * + * Blacklisted hosts will use an alternate lookup method: + * - Twitpic + * + * Whitelisted hosts will use known oEmbed API endpoints: + * - Flickr, YFrog + * + * Sites that provide discovery links will use them directly; a bug + * in use of discovery links with query strings is worked around. + * + * Others will fall back to oohembed (unless disabled). + * The API endpoint can be configured or disabled through config + * as 'oohembed'/'endpoint'. + */ +class oEmbedHelper +{ + protected static $apiMap = array( + 'flickr.com' => 'http://www.flickr.com/services/oembed/', + 'yfrog.com' => 'http://www.yfrog.com/api/oembed', + ); + protected static $functionMap = array( + 'twitpic.com' => 'oEmbedHelper::twitPic', + ); + + /** + * Perform or fake an oEmbed lookup for the given resource. + * + * Some known hosts are whitelisted with API endpoints where we + * know they exist but autodiscovery data isn't available. + * If autodiscovery links are missing and we don't recognize the + * host, we'll pass it to oohembed.com's public service which + * will either proxy or fake info on a lot of sites. + * + * A few hosts are blacklisted due to known problems with oohembed, + * in which case we'll look up the info another way and return + * equivalent data. + * + * Throws exceptions on failure. + * + * @param string $url + * @param array $params + * @return object + */ + public static function getObject($url, $params=array()) + { + common_log(LOG_INFO, 'QQQ: wtf? ' . $url); + $host = parse_url($url, PHP_URL_HOST); + if (substr($host, 0, 4) == 'www.') { + $host = substr($host, 4); + } + + // Blacklist: systems with no oEmbed API of their own, which are + // either missing from or broken on oohembed.com's proxy. + // we know how to look data up in another way... + if (array_key_exists($host, self::$functionMap)) { + $func = self::$functionMap[$host]; + return call_user_func($func, $url, $params); + } + + // Whitelist: known API endpoints for sites that don't provide discovery... + if (array_key_exists($host, self::$apiMap)) { + $api = self::$apiMap[$host]; + common_log(LOG_INFO, 'QQQ: going to: ' . $api); + } else { + $api = false; + common_log(LOG_INFO, 'QQQ: no map for ' . $host); + } + return self::getObjectFrom($api, $url, $params); + } + + /** + * Actually do an oEmbed lookup to a particular API endpoint, + * or to the autodiscovered target, or to oohembed. + * + * @param mixed $api string or false: oEmbed API endpoint URL + * @param string $url target URL to look up info about + * @param array $params + * @return object + */ + static protected function getObjectFrom($api, $url, $params=array()) + { + $options = array(); + if ($api) { + $options[Services_oEmbed::OPTION_API] = $api; + } + + try { + $oEmbed = new Services_oEmbed_Tweaked($url, $options); + } catch (Services_oEmbed_Exception_NoSupport $e) { + // Discovery failed... fall back to oohembed if enabled. + $oohembed = common_config('oohembed', 'endpoint'); + if ($oohembed) { + $options[Services_oEmbed::OPTION_API] = $oohembed; + $oEmbed = new Services_oEmbed_Tweaked($url, $options); + } else { + throw $e; + } + } + + // And.... let's go look it up! + return $oEmbed->getObject($params); + } + + /** + * Using a local function for twitpic lookups, as oohembed's adapter + * doesn't return a valid result: + * http://code.google.com/p/oohembed/issues/detail?id=19 + * + * This code fetches metadata from Twitpic's own API, and attempts + * to guess proper thumbnail size from the original's size. + * + * @todo respect maxwidth and maxheight params + * + * @param string $url + * @param array $params + * @return object + */ + static function twitPic($url, $params=array()) + { + $matches = array(); + if (preg_match('!twitpic\.com/(\w+)!', $url, $matches)) { + $id = $matches[1]; + } else { + throw new Exception("Invalid twitpic URL"); + } + + // Grab metadata from twitpic's API... + // http://dev.twitpic.com/docs/2/media_show + $data = self::json('http://api.twitpic.com/2/media/show.json', + array('id' => $id)); + $oembed = (object)array('type' => 'photo', + 'url' => 'http://twitpic.com/show/full/' . $data->short_id, + 'width' => $data->width, + 'height' => $data->height); + if (!empty($data->message)) { + $oembed->title = $data->message; + } + + // Thumbnail is cropped and scaled to 150x150 box: + // http://dev.twitpic.com/docs/thumbnails/ + $thumbSize = 150; + $oembed->thumbnail_url = 'http://twitpic.com/show/thumb/' . $data->short_id; + $oembed->thumbnail_width = $thumbSize; + $oembed->thumbnail_height = $thumbSize; + + return $oembed; + } + + /** + * Fetch some URL and return JSON data. + * + * @param string $url + * @param array $params query-string params + * @return object + */ + static protected function json($url, $params=array()) + { + $data = self::http($url, $params); + return json_decode($data); + } + + /** + * Hit some web API and return data on success. + * @param string $url + * @param array $params + * @return string + */ + static protected function http($url, $params=array()) + { + $client = HTTPClient::start(); + if ($params) { + $query = http_build_query($params, null, '&'); + if (strpos($url, '?') === false) { + $url .= '?' . $query; + } else { + $url .= '&' . $query; + } + } + $response = $client->get($url); + if ($response->isOk()) { + return $response->getBody(); + } else { + throw new Exception('Bad HTTP response code: ' . $response->getStatus()); + } + } +} + +class Services_oEmbed_Tweaked extends Services_oEmbed +{ + protected function discover($url) + { + $api = parent::discover($url); + if (strpos($api, '?') !== false) { + // Services_oEmbed doesn't expect to find existing params + // on its API endpoint, which may surprise you since the + // spec says discovery URLs should include parameters... :) + // + // Appending a '&' on the end keeps the later-appended '?' + // from breaking whatever the first parameters was. + return $api . '&'; + } + return $api; + } + + public function getObject(array $params = array()) + { + $api = $this->options[self::OPTION_API]; + if (strpos($api, '?') !== false) { + // The Services_oEmbed code appends a '?' on the end, which breaks + // the next parameter which may be something important like + // maxwidth. + // + // Sticking this bogus entry into our parameters gets us past it. + $params = array_merge(array('statusnet' => 1), $params); + } + return parent::getObject($params); + } + +} \ No newline at end of file