code:
<?php

if (php_sapi_name() != 'cli') {
    die('not for web');
}

define('HTTP_TIMEOUT', 60); // ssslllloowwwww salmon if queues are off

define('INSTALLDIR', dirname(dirname(dirname(dirname(__FILE__)))));
set_include_path(INSTALLDIR . '/extlib' . PATH_SEPARATOR . get_include_path());

require_once 'PEAR.php';
require_once 'Net/URL2.php';
require_once 'HTTP/Request2.php';

// ostatus test script, client-side :)

class TestBase
{
    function log($str)
    {
        $args = func_get_args();
        array_shift($args);

        $msg = vsprintf($str, $args);
        print $msg . "\n";
    }

    function assertEqual($a, $b)
    {
        if ($a != $b) {
            throw new Exception("Failed to assert equality: expected $a, got $b");
        }
        return true;
    }

    function assertNotEqual($a, $b)
    {
        if ($a == $b) {
            throw new Exception("Failed to assert inequality: expected not $a, got $b");
        }
        return true;
    }

    function assertTrue($a)
    {
        if (!$a) {
            throw new Exception("Failed to assert true: got false");
        }
    }

    function assertFalse($a)
    {
        if ($a) {
            throw new Exception("Failed to assert false: got true");
        }
    }
}

class OStatusTester extends TestBase
{
    /**
     * @param string $a base URL of test site A (eg http://localhost/mublog)
     * @param string $b base URL of test site B (eg http://localhost/mublog2)
     * @param int $timeout HTTP timeout value (needs to be long if Salmon is slow)
     */
    function __construct($a, $b, $timeout=60) {
        $this->a = $a;
        $this->b = $b;

        $base = 'test' . mt_rand(1, 1000000);
        $this->pub = new SNTestClient($this->a, 'pub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);
        $this->sub = new SNTestClient($this->b, 'sub' . $base, 'pw-' . mt_rand(1, 1000000), $timeout);

        $this->group = 'group' . $base;
    }

    function run()
    {
        $this->setup();

        $methods = get_class_methods($this);
        foreach ($methods as $method) {
            if (strtolower(substr($method, 0, 4)) == 'test') {
                print "\n";
                print "== $method ==\n";
                call_user_func(array($this, $method));
            }
        }

        print "\n";
        $this->log("DONE!");
    }

    function setup()
    {
        $this->pub->register();
        $this->pub->assertRegistered();

        $this->sub->register();
        $this->sub->assertRegistered();
    }

    function testLocalPost()
    {
        $post = $this->pub->post("Local post, no subscribers yet.");
        $this->assertNotEqual('', $post);

        $post = $this->sub->post("Local post, no subscriptions yet.");
        $this->assertNotEqual('', $post);
    }

    /**
     * pub posts: @b/sub
     */
    function testMentionUrl()
    {
        $bits = parse_url($this->b);
        $base = $bits['host'];
        if (isset($bits['path'])) {
            $base .= $bits['path'];
        }
        $name = $this->sub->username;

        $post = $this->pub->post("@$base/$name should have this in home and replies");
        $this->sub->assertReceived($post);
    }

    function testSubscribe()
    {
        $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
        $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
        $this->sub->subscribe($this->pub->getProfileLink());
        $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
        $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
    }

    function testPush()
    {
        $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
        $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));

        $name = $this->sub->username;
        $post = $this->pub->post("Regular post, which $name should get via WebSub");
        $this->sub->assertReceived($post);
    }

    function testMentionSubscribee()
    {
        $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
        $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri()));

        $name = $this->pub->username;
        $post = $this->sub->post("Just a quick note back to my remote subscribee @$name");
        $this->pub->assertReceived($post);
    }

    function testUnsubscribe()
    {
        $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri()));
        $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri()));
        $this->sub->unsubscribe($this->pub->getProfileLink());
        $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri()));
        $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri()));
    }

    function testCreateGroup()
    {
        $this->groupUrl = $this->pub->createGroup($this->group);
        $this->assertTrue(!empty($this->groupUrl));
    }

    function testJoinGroup()
    {
        #$this->assertFalse($this->sub->inGroup($this->groupUrl));
        $this->sub->joinGroup($this->groupUrl);
        #$this->assertTrue($this->sub->inGroup($this->groupUrl));
    }

    function testLocalGroupPost()
    {
        $post = $this->pub->post("Group post from local to !{$this->group}, should go out over push.");
        $this->assertNotEqual('', $post);
        $this->sub->assertReceived($post);
    }

    function testRemoteGroupPost()
    {
        $post = $this->sub->post("Group post from remote to !{$this->group}, should come in over salmon.");
        $this->assertNotEqual('', $post);
        $this->pub->assertReceived($post);
    }

    function testLeaveGroup()
    {
        #$this->assertTrue($this->sub->inGroup($this->groupUrl));
        $this->sub->leaveGroup($this->groupUrl);
        #$this->assertFalse($this->sub->inGroup($this->groupUrl));
    }
}

class SNTestClient extends TestBase
{
    function __construct($base, $username, $password, $timeout=60)
    {
        $this->basepath = $base;
        $this->username = $username;
        $this->password = $password;
        $this->timeout = $timeout;

        $this->fullname = ucfirst($username) . ' Smith';
        $this->homepage = 'http://example.org/' . $username;
        $this->bio = 'Stub account for OStatus tests.';
        $this->location = 'Montreal, QC';
    }

    /**
     * Make a low-level web hit to this site, with authentication.
     * @param string $path URL fragment for something under the base path
     * @param array $params POST parameters to send
     * @param boolean $auth whether to include auth data
     * @return string
     * @throws Exception on low-level error conditions
     */
    protected function hit($path, $params=array(), $auth=false, $cookies=array())
    {
        $url = $this->basepath . '/' . $path;

        $http = new HTTP_Request2($url, 'POST', array('timeout' => $this->timeout));
        if ($auth) {
            $http->setAuth($this->username, $this->password, HTTP_Request2::AUTH_BASIC);
        }
        foreach ($cookies as $name => $val) {
            $http->addCookie($name, $val);
        }
        $http->addPostParameter($params);
        $response = $http->send();

        $code = $response->getStatus();
        if ($code < '200' || $code >= '400') {
            throw new Exception("Failed API hit to $url: $code\n" . $response->getBody());
        }

        return $response;
    }

    /**
     * Make a hit to a web form, without authentication but with a session.
     * @param string $path URL fragment relative to site base
     * @param string $form id of web form to pull initial parameters from
     * @param array $params POST parameters, will be merged with defaults in form
     */
    protected function web($path, $form, $params=array())
    {
        $url = $this->basepath . '/' . $path;
        $http = new HTTP_Request2($url, 'GET', array('timeout' => $this->timeout));
        $response = $http->send();

        $dom = $this->checkWeb($url, 'GET', $response);
        $cookies = array();
        foreach ($response->getCookies() as $cookie) {
            // @fixme check for expirations etc
            $cookies[$cookie['name']] = $cookie['value'];
        }

        $form = $dom->getElementById($form);
        if (!$form) {
            throw new Exception("Form $form not found on $url");
        }
        $inputs = $form->getElementsByTagName('input');
        foreach ($inputs as $item) {
            $type = $item->getAttribute('type');
            if ($type != 'check') {
                $name = $item->getAttribute('name');
                $val = $item->getAttribute('value');
                if ($name && $val && !isset($params[$name])) {
                    $params[$name] = $val;
                }
            }
        }

        $response = $this->hit($path, $params, false, $cookies);
        $dom = $this->checkWeb($url, 'POST', $response);

        return $dom;
    }

    protected function checkWeb($url, $method, $response)
    {
        $dom = new DOMDocument();
        if (!$dom->loadHTML($response->getBody())) {
            throw new Exception("Invalid HTML from $method to $url");
        }

        $xpath = new DOMXPath($dom);
        $error = $xpath->query('//p[@class="error"]');
        if ($error && $error->length) {
            throw new Exception("Error on $method to $url: " .
                                $error->item(0)->textContent);
        }

        return $dom;
    }

    protected function parseXml($path, $body)
    {
        $dom = new DOMDocument();
        if ($dom->loadXML($body)) {
            return $dom;
        } else {
            throw new Exception("Bogus XML data from $path:\n$body");
        }
    }

    /**
     * Make a hit to a REST-y XML page on the site, without authentication.
     * @param string $path URL fragment for something relative to base
     * @param array $params POST parameters to send
     * @return DOMDocument
     * @throws Exception on low-level error conditions
     */
    protected function xml($path, $params=array())
    {
        $response = $this->hit($path, $params, true);
        $body = $response->getBody();
        return $this->parseXml($path, $body);
    }

    protected function parseJson($path, $body)
    {
        $data = json_decode($body, true);
        if ($data !== null) {
            if (!empty($data['error'])) {
                throw new Exception("JSON API returned error: " . $data['error']);
            }
            return $data;
        } else {
            throw new Exception("Bogus JSON data from $path:\n$body");
        }
    }

    /**
     * Make an API hit to this site, with authentication.
     * @param string $path URL fragment for something under 'api' folder
     * @param string $style one of 'json', 'xml', or 'atom'
     * @param array $params POST parameters to send
     * @return mixed associative array for JSON, DOMDocument for XML/Atom
     * @throws Exception on low-level error conditions
     */
    protected function api($path, $style, $params=array())
    {
        $response = $this->hit("api/$path.$style", $params, true);
        $body = $response->getBody();
        if ($style == 'json') {
            return $this->parseJson($path, $body);
        } else if ($style == 'xml' || $style == 'atom') {
            return $this->parseXml($path, $body);
        } else {
            throw new Exception("API needs to be JSON, XML, or Atom");
        }
    }

    /**
     * Register the account.
     *
     * Unfortunately there's not an API method for registering, so we fake it.
     */
    function register()
    {
        $this->log("Registering user %s on %s",
                   $this->username,
                   $this->basepath);
        $ret = $this->web('main/register', 'form_register',
            array('nickname' => $this->username,
                  'password' => $this->password,
                  'confirm' => $this->password,
                  'fullname' => $this->fullname,
                  'homepage' => $this->homepage,
                  'bio' => $this->bio,
                  'license' => 1,
                  'submit' => 'Register'));
    }

    /**
     * @return string canonical URI/URL to profile page
     */
    function getProfileUri()
    {
        $data = $this->api('account/verify_credentials', 'json');
        $id = $data['id'];
        return $this->basepath . '/user/' . $id;
    }

    /**
     * @return string human-friendly URL to profile page
     */
    function getProfileLink()
    {
        return $this->basepath . '/' . $this->username;
    }

    /**
     * Check that the account has been registered and can be used.
     * On failure, throws a test failure exception.
     */
    function assertRegistered()
    {
        $this->log("Confirming %s is registered on %s",
                   $this->username,
                   $this->basepath);
        $data = $this->api('account/verify_credentials', 'json');
        $this->assertEqual($this->username, $data['screen_name']);
        $this->assertEqual($this->fullname, $data['name']);
        $this->assertEqual($this->homepage, $data['url']);
        $this->assertEqual($this->bio, $data['description']);
        $this->log("  looks good!");
    }

    /**
     * Post a given message from this account
     * @param string $message
     * @return string URL/URI of notice
     * @todo reply, location options
     */
    function post($message)
    {
        $this->log("Posting notice as %s on %s: %s",
                   $this->username,
                   $this->basepath,
                   $message);
        $data = $this->api('statuses/update', 'json',
            array('status' => $message));

        $url = $this->basepath . '/notice/' . $data['id'];
        return $url;
    }

    /**
     * Check that this account has received the notice.
     * @param string $notice_uri URI for the notice to check for
     */
    function assertReceived($notice_uri)
    {
        $timeout = 5;
        $tries = 6;
        while ($tries) {
            $ok = $this->checkReceived($notice_uri);
            if ($ok) {
                return true;
            }
            $tries--;
            if ($tries) {
                $this->log("  didn't see it yet, waiting $timeout seconds");
                sleep($timeout);
            }
        }
        throw new Exception("  message $notice_uri not received by $this->username");
    }

    /**
     * Pull the user's home timeline to check if a notice with the given
     * source URL has been received recently.
     * If we don't see it, we'll try a couple more times up to 10 seconds.
     *
     * @param string $notice_uri
     */
    function checkReceived($notice_uri)
    {
        $this->log("Checking if %s on %s received notice %s",
                   $this->username,
                   $this->basepath,
                   $notice_uri);
        $params = array();
        $dom = $this->api('statuses/home_timeline', 'atom', $params);

        $xml = simplexml_import_dom($dom);
        if (!$xml->entry) {
            return false;
        }
        if (is_array($xml->entry)) {
            $entries = $xml->entry;
        } else {
            $entries = array($xml->entry);
        }
        foreach ($entries as $entry) {
            if ($entry->id == $notice_uri) {
                $this->log("  found it $notice_uri");
                return true;
            }
        }
        return false;
    }

    /**
     * @param string $profile user page link or webfinger
     */
    function subscribe($profile)
    {
        // This uses the command interface, since there's not currently
        // a friendly Twit-API way to do a fresh remote subscription and
        // the web form's a pain to use.
        $this->post('follow ' . $profile);
    }

    /**
     * @param string $profile user page link or webfinger
     */
    function unsubscribe($profile)
    {
        // This uses the command interface, since there's not currently
        // a friendly Twit-API way to do a fresh remote subscription and
        // the web form's a pain to use.
        $this->post('leave ' . $profile);
    }

    /**
     * Check that this account is subscribed to the given profile.
     * @param string $profile_uri URI for the profile to check for
     * @return boolean
     */
    function hasSubscription($profile_uri)
    {
        $this->log("Checking if $this->username has a subscription to $profile_uri");

        $me = $this->getProfileUri();
        return $this->checkSubscription($me, $profile_uri);
    }

    /**
     * Check that this account is subscribed to by the given profile.
     * @param string $profile_uri URI for the profile to check for
     * @return boolean
     */
    function hasSubscriber($profile_uri)
    {
        $this->log("Checking if $this->username is subscribed to by $profile_uri");

        $me = $this->getProfileUri();
        return $this->checkSubscription($profile_uri, $me);
    }

    protected function checkSubscription($subscriber, $subscribed)
    {
        // Using FOAF as the API methods for checking the social graph
        // currently are unfriendly to remote profiles
        $ns_foaf = 'http://xmlns.com/foaf/0.1/';
        $ns_sioc = 'http://rdfs.org/sioc/ns#';
        $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';

        $dom = $this->xml($this->username . '/foaf');
        $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent');
        foreach ($agents as $agent) {
            $agent_uri = $agent->getAttributeNS($ns_rdf, 'about');
            if ($agent_uri == $subscriber) {
                $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows');
                foreach ($follows as $follow) {
                    $target = $follow->getAttributeNS($ns_rdf, 'resource');
                    if ($target == ($subscribed . '#acct')) {
                        $this->log("  confirmed $subscriber subscribed to $subscribed");
                        return true;
                    }
                }
                $this->log("  we found $subscriber but they don't follow $subscribed");
                return false;
            }
        }
        $this->log("  can't find $subscriber in {$this->username}'s social graph.");
        return false;
    }

    /**
     * Create a group on this site.
     *
     * @param string $nickname
     * @param array $options
     * @return string: profile URL for the group
     */
    function createGroup($nickname, $options=array()) {
        $this->log("Creating group as %s on %s: %s",
                   $this->username,
                   $this->basepath,
                   $nickname);

        $data = $this->api('statusnet/groups/create', 'json',
            array_merge(array('nickname' => $nickname), $options));
        $url = $data['url'];

        if ($url) {
            $this->log('  created as %s', $url);
        } else {
            $this->log('  failed? %s', var_export($data, true));
        }
        return $url;
    }

    function groupInfo($nickname) {
        $data = $this->api('statusnet/groups/show', 'json', array(
            'id' => $nickname
        ));
    }

    /**
     * Join a group.
     *
     * @param string $group nickname or URL
     */
    function joinGroup($group) {
        $this->post('join ' . $group);
    }

    /**
     * Leave a group.
     *
     * @param string $group nickname or URL
     */
    function leaveGroup($group) {
        $this->post('drop ' . $group);
    }

    /**
     *
     * @param string $nickname
     * @return
     */
    function inGroup($nickname) {
        // @todo
    }
}

// @fixme switch to commandline.inc?
$timeout = HTTP_TIMEOUT;

$args = array();
$options = array();
foreach (array_slice($_SERVER['argv'], 1) as $arg) {
    if (substr($arg, 0, 2) == '--') {
        $bits = explode('=', substr($arg, 2), 2);
        if (count($bits == 2)) {
            list($key, $val) = $bits;
            $options[$key] = $val;
        } else {
            list($key) = $bits;
            $options[$key] = true;
        }
    } else {
        $args[] = $arg;
    }
}
if (count($args) < 2) {
    print <<<END_HELP
remote-tests.php [options] <url1> <url2>
  --timeout=## change HTTP timeout from default {$timeout}s
  url1: base URL of a StatusNet instance
  url2: base URL of another StatusNet instance

This will register user accounts on the two given StatusNet instances
and run some tests to confirm that OStatus subscription and posting
between the two sites works correctly.

END_HELP;
exit(1);
}

$a = $args[0];
$b = $args[1];
if (isset($options['timeout'])) {
    $timeout = intval($options['timeout']);
}

$tester = new OStatusTester($a, $b, $timeout);
$tester->run();


code:
#!/usr/bin/env php
<?php
/*
 * StatusNet - a distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));

$longoptions = array('verify', 'slap=', 'notice=');

$helptext = <<<END_OF_HELP
slap.php [options]

Test generation and sending of magic envelopes for Salmon slaps.

     --notice=N   generate entry for this notice number
     --verify     send signed magic envelope to Tuomas Koski's test service
     --slap=<url> send signed Salmon slap to the destination endpoint

END_OF_HELP;

require_once INSTALLDIR.'/scripts/commandline.inc';

if (!have_option('--notice')) {
    print "$helptext";
    exit(1);
}

$notice_id = get_option_value('--notice');

$notice = Notice::getKV('id', $notice_id);
$profile = $notice->getProfile();
$entry = $notice->asAtomEntry(true);

echo "== Original entry ==\n\n";
print $entry;
print "\n\n";

$magic_env = MagicEnvelope::signAsUser($entry, $profile->getUser());
$envxml = $magic_env->toXML();

echo "== Signed envelope ==\n\n";
print $envxml;
print "\n\n";

echo "== Testing local verification ==\n\n";
$magic_env = new MagicEnvelope($envxml);
$activity = new Activity($magic_env->getPayload()->documentElement);
$actprofile = Profile::fromUri($activity->actor->id);
$ok = $magic_env->verify($actprofile);
if ($ok) {
    print "OK\n\n";
} else {
    print "FAIL\n\n";
}

if (have_option('--verify')) {
    $url = 'http://www.madebymonsieur.com/ostatus_discovery/magic_env/validate/';
    echo "== Testing remote verification ==\n\n";
    print "Sending for verification to $url ...\n";

    $client = new HTTPClient();
    try {
        $response = $client->post($url, array(), array('magic_env' => $envxml));

        print $response->getStatus() . "\n\n";
        print $response->getBody() . "\n\n";
    } catch (HTTP_Request2_Exception $e) {
        print 'Failed POST to URL '.var_export($url, true).': '.$e->getMessage();
    }
}

if (have_option('--slap')) {
    $url = get_option_value('--slap');
    echo "== Remote salmon slap ==\n\n";
    print "Sending signed Salmon slap to $url ...\n";

    $ok = Salmon::post($url, $entry, $profile->getUser());
    if ($ok) {
        print "OK\n\n";
    } else {
        print "FAIL\n\n";
    }
}

repo_name: andremichelle/gnu

license: mit


code:
So far this is still in $config['site']['oembed'].

oembed
--------

oEmbed endpoint for multimedia attachments (links in posts). Will also
work as 'oohembed' for backwards compatibility.

endpoint: oohembed endpoint using http://oohembed.com/ software. Defaults to
	  'http://oohembed.com/oohembed/'.
order: Array of methods to check for OEmbed data. Methods include 'built-in'
       (use a built-in function to simulate oEmbed for some sites),
       'well-known' (use well-known public oEmbed endpoints),
       'discovery' (discover using <link> headers in HTML), 'service' (use
       a third-party service, like oohembed or embed.ly. Default is
       array('built-in', 'well-known', 'service', 'discovery'). Note that very
       few sites implement oEmbed; 'discovery' is going to fail 99% of the
       time.

path: plugins/Oembed/CONFIGURE


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * OembedPlugin implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Stephen Paul Weber
 * @author    hannes
 * @author    Mikael Nordfeldth
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

/**
 * Base class for the oEmbed plugin that does most of the heavy lifting to get
 * and display representations for remote content.
 *
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class OembedPlugin extends Plugin
{
    const PLUGIN_VERSION = '3.0.1';

    // settings which can be set in config.php with addPlugin('Oembed', array('param'=>'value', ...));
    // WARNING, these are _regexps_ (slashes added later). Always escape your dots and end your strings
    public $domain_whitelist = array(       // hostname => service provider
                                    '^i\d*\.ytimg\.com$' => 'YouTube',
                                    '^i\d*\.vimeocdn\.com$' => 'Vimeo',
                                    );
    public $append_whitelist = array();  // fill this array as domain_whitelist to add more trusted sources
    public $check_whitelist  = false;    // security/abuse precaution
    public $keep_original = false;

    protected $imgData = array();

    /**
     * Initialize the oEmbed plugin and set up the environment it needs for it.
     * Returns true if it initialized properly, the exception object if it
     * doesn't.
     */
    public function initialize()
    {
        parent::initialize();

        $this->domain_whitelist = array_merge($this->domain_whitelist, $this->append_whitelist);
    }

    /**
     * The code executed on GNU social checking the database schema, which in
     * this case is to make sure we have the plugin table we need.
     *
     * @return bool true if it ran successfully, the exception object if it doesn't.
     */
    public function onCheckSchema()
    {
        $schema = Schema::get();
        $schema->ensureTable('file_oembed', File_oembed::schemaDef());
        return true;
    }

    /**
     * This code executes when GNU social creates the page routing, and we hook
     * on this event to add our action handler for oEmbed.
     *
     * @param $m URLMapper the router that was initialized.
     * @return bool true if successful, the exception object if it isn't.
     */
    public function onRouterInitialized(URLMapper $m)
    {
        $m->connect('main/oembed', array('action' => 'oembed'));
    }

    /**
     * This event executes when GNU social encounters a remote URL we then decide
     * to interrogate for metadata.  oEmbed gloms onto it to see if we have an
     * oEmbed endpoint or image to try to represent in the post.
     *
     * @param $url string        the remote URL we're looking at
     * @param $dom DOMDocument   the document we're getting metadata from
     * @param $metadata stdClass class representing the metadata
     * @return bool true if successful, the exception object if it isn't.
     */
    public function onGetRemoteUrlMetadataFromDom($url, DOMDocument $dom, stdClass &$metadata)
    {
        try {
            common_log(LOG_INFO, 'Trying to discover an oEmbed endpoint using link headers.');
            $api = oEmbedHelper::oEmbedEndpointFromHTML($dom);
            common_log(LOG_INFO, 'Found oEmbed API endpoint ' . $api . ' for URL ' . $url);
            $params = array(
                'maxwidth' => common_config('thumbnail', 'width'),
                'maxheight' => common_config('thumbnail', 'height'),
            );
            $metadata = oEmbedHelper::getOembedFrom($api, $url, $params);

            // Facebook just gives us javascript in its oembed html,
            // so use the content of the title element instead
            if (strpos($url, 'https://www.facebook.com/') === 0) {
                $metadata->html = @$dom->getElementsByTagName('title')->item(0)->nodeValue;
            }

            // Wordpress sometimes also just gives us javascript, use og:description if it is available
            $xpath = new DomXpath($dom);
            $generatorNode = @$xpath->query('//meta[@name="generator"][1]')->item(0);
            if ($generatorNode instanceof DomElement) {
                // when wordpress only gives us javascript, the html stripped from tags
                // is the same as the title, so this helps us to identify this (common) case
                if (strpos($generatorNode->getAttribute('content'), 'WordPress') === 0
                && trim(strip_tags($metadata->html)) == trim($metadata->title)) {
                    $propertyNode = @$xpath->query('//meta[@property="og:description"][1]')->item(0);
                    if ($propertyNode instanceof DomElement) {
                        $metadata->html = $propertyNode->getAttribute('content');
                    }
                }
            }
        } catch (Exception $e) {
            // FIXME - make sure the error was because we couldn't get metadata, not something else! -mb
            common_log(LOG_INFO, 'Could not find an oEmbed endpoint using link headers, trying OpenGraph from HTML.');
            // Just ignore it!
            $metadata = OpenGraphHelper::ogFromHtml($dom);
        }

        if (isset($metadata->thumbnail_url)) {
            // sometimes sites serve the path, not the full URL, for images
            // let's "be liberal in what you accept from others"!
            // add protocol and host if the thumbnail_url starts with /
            if (substr($metadata->thumbnail_url, 0, 1) == '/') {
                $thumbnail_url_parsed = parse_url($metadata->url);
                $metadata->thumbnail_url = $thumbnail_url_parsed['scheme']."://".$thumbnail_url_parsed['host'].$metadata->thumbnail_url;
            }

            // some wordpress opengraph implementations sometimes return a white blank image
            // no need for us to save that!
            if ($metadata->thumbnail_url == 'https://s0.wp.com/i/blank.jpg') {
                unset($metadata->thumbnail_url);
            }

            // FIXME: this is also true of locally-installed wordpress so we should watch out for that.
        }
        return true;
    }

    public function onEndShowHeadElements(Action $action)
    {
        switch ($action->getActionName()) {
        case 'attachment':
            $action->element('link', array('rel'=>'alternate',
                'type'=>'application/json+oembed',
                'href'=>common_local_url(
                    'oembed',
                    array(),
                    array('format'=>'json', 'url'=>
                        common_local_url(
                            'attachment',
                            array('attachment' => $action->attachment->getID())
                        ))
                ),
                'title'=>'oEmbed'));
            $action->element('link', array('rel'=>'alternate',
                'type'=>'text/xml+oembed',
                'href'=>common_local_url(
                    'oembed',
                    array(),
                    array('format'=>'xml','url'=>
                        common_local_url(
                            'attachment',
                            array('attachment' => $action->attachment->getID())
                        ))
                ),
                'title'=>'oEmbed'));
            break;
        case 'shownotice':
            if (!$action->notice->isLocal()) {
                break;
            }
            try {
                $action->element('link', array('rel'=>'alternate',
                    'type'=>'application/json+oembed',
                    'href'=>common_local_url(
                        'oembed',
                        array(),
                        array('format'=>'json','url'=>$action->notice->getUrl())
                    ),
                    'title'=>'oEmbed'));
                $action->element('link', array('rel'=>'alternate',
                    'type'=>'text/xml+oembed',
                    'href'=>common_local_url(
                        'oembed',
                        array(),
                        array('format'=>'xml','url'=>$action->notice->getUrl())
                    ),
                    'title'=>'oEmbed'));
            } catch (InvalidUrlException $e) {
                // The notice is probably a share or similar, which don't
                // have a representational URL of their own.
            }
            break;
        }

        return true;
    }

    public function onEndShowStylesheets(Action $action)
    {
        $action->cssLink($this->path('css/oembed.css'));
        return true;
    }

    /**
     * Save embedding information for a File, if applicable.
     *
     * Normally this event is called through File::saveNew()
     *
     * @param File   $file       The newly inserted File object.
     *
     * @return boolean success
     */
    public function onEndFileSaveNew(File $file)
    {
        $fo = File_oembed::getKV('file_id', $file->getID());
        if ($fo instanceof File_oembed) {
            common_log(LOG_WARNING, "Strangely, a File_oembed object exists for new file {$file->getID()}", __FILE__);
            return true;
        }

        if (isset($file->mimetype)
            && (('text/html' === substr($file->mimetype, 0, 9)
            || 'application/xhtml+xml' === substr($file->mimetype, 0, 21)))) {
            try {
                $oembed_data = File_oembed::_getOembed($file->url);
                if ($oembed_data === false) {
                    throw new Exception('Did not get oEmbed data from URL');
                }
                $file->setTitle($oembed_data->title);
            } catch (Exception $e) {
                common_log(LOG_WARNING, sprintf(__METHOD__.': %s thrown when getting oEmbed data: %s', get_class($e), _ve($e->getMessage())));
                return true;
            }

            File_oembed::saveNew($oembed_data, $file->getID());
        }
        return true;
    }

    public function onEndShowAttachmentLink(HTMLOutputter $out, File $file)
    {
        $oembed = File_oembed::getKV('file_id', $file->getID());
        if (empty($oembed->author_name) && empty($oembed->provider)) {
            return true;
        }
        $out->elementStart('div', array('id'=>'oembed_info', 'class'=>'e-content'));
        if (!empty($oembed->author_name)) {
            $out->elementStart('div', 'fn vcard author');
            if (empty($oembed->author_url)) {
                $out->text($oembed->author_name);
            } else {
                $out->element(
                    'a',
                    array('href' => $oembed->author_url,
                                         'class' => 'url'),
                    $oembed->author_name
                );
            }
        }
        if (!empty($oembed->provider)) {
            $out->elementStart('div', 'fn vcard');
            if (empty($oembed->provider_url)) {
                $out->text($oembed->provider);
            } else {
                $out->element(
                    'a',
                    array('href' => $oembed->provider_url,
                                         'class' => 'url'),
                    $oembed->provider
                );
            }
        }
        $out->elementEnd('div');
    }

    public function onFileEnclosureMetadata(File $file, &$enclosure)
    {
        // Never treat generic HTML links as an enclosure type!
        // But if we have oEmbed info, we'll consider it golden.
        $oembed = File_oembed::getKV('file_id', $file->getID());
        if (!$oembed instanceof File_oembed || !in_array($oembed->type, array('photo', 'video'))) {
            return true;
        }

        foreach (array('mimetype', 'url', 'title', 'modified', 'width', 'height') as $key) {
            if (isset($oembed->{$key}) && !empty($oembed->{$key})) {
                $enclosure->{$key} = $oembed->{$key};
            }
        }
        return true;
    }

    public function onStartShowAttachmentRepresentation(HTMLOutputter $out, File $file)
    {
        try {
            $oembed = File_oembed::getByFile($file);
        } catch (NoResultException $e) {
            return true;
        }

        // Show thumbnail as usual if it's a photo.
        if ($oembed->type === 'photo') {
            return true;
        }

        $out->elementStart('article', ['class'=>'h-entry oembed']);
        $out->elementStart('header');
        try {
            $thumb = $file->getThumbnail(128, 128);
            $out->element('img', $thumb->getHtmlAttrs(['class'=>'u-photo oembed']));
            unset($thumb);
        } catch (Exception $e) {
            $out->element('div', ['class'=>'error'], $e->getMessage());
        }
        $out->elementStart('h5', ['class'=>'p-name oembed']);
        $out->element('a', ['class'=>'u-url', 'href'=>$file->getUrl()], common_strip_html($oembed->title));
        $out->elementEnd('h5');
        $out->elementStart('div', ['class'=>'p-author oembed']);
        if (!empty($oembed->author_name)) {
            // TRANS: text before the author name of oEmbed attachment representation
            // FIXME: The whole "By x from y" should be i18n because of different language constructions.
            $out->text(_('By '));
            $attrs = ['class'=>'h-card p-author'];
            if (!empty($oembed->author_url)) {
                $attrs['href'] = $oembed->author_url;
                $tag = 'a';
            } else {
                $tag = 'span';
            }
            $out->element($tag, $attrs, $oembed->author_name);
        }
        if (!empty($oembed->provider)) {
            // TRANS: text between the oEmbed author name and provider url
            // FIXME: The whole "By x from y" should be i18n because of different language constructions.
            $out->text(_(' from '));
            $attrs = ['class'=>'h-card'];
            if (!empty($oembed->provider_url)) {
                $attrs['href'] = $oembed->provider_url;
                $tag = 'a';
            } else {
                $tag = 'span';
            }
            $out->element($tag, $attrs, $oembed->provider);
        }
        $out->elementEnd('div');
        $out->elementEnd('header');
        $out->elementStart('div', ['class'=>'p-summary oembed']);
        $out->raw(common_purify($oembed->html));
        $out->elementEnd('div');
        $out->elementStart('footer');
        $out->elementEnd('footer');
        $out->elementEnd('article');

        return false;
    }

    public function onShowUnsupportedAttachmentRepresentation(HTMLOutputter $out, File $file)
    {
        try {
            $oembed = File_oembed::getByFile($file);
        } catch (NoResultException $e) {
            return true;
        }

        // the 'photo' type is shown through ordinary means, using StartShowAttachmentRepresentation!
        switch ($oembed->type) {
        case 'video':
        case 'link':
            if (!empty($oembed->html)
                    && (GNUsocial::isAjax() || common_config('attachments', 'show_html'))) {
                $purifier = new \HTMLPurifier();
                // FIXME: do we allow <object> and <embed> here? we did that when we used htmLawed, but I'm not sure anymore...
                $out->raw($purifier->purify($oembed->html));
            }
            return false;
        }

        return true;
    }

    /**
     * This event executes when GNU social is creating a file thumbnail entry in
     * the database.  We glom onto this to create proper information for oEmbed
     * object thumbnails.
     *
     * @param $file File the file of the created thumbnail
     * @param &$imgPath string = the path to the created thumbnail
     * @return bool true if it succeeds (including non-action
     * states where it isn't oEmbed data, so it doesn't mess up the event handle
     * for other things hooked into it), or the exception if it fails.
     */
    public function onCreateFileImageThumbnailSource(File $file, &$imgPath)
    {
        // If we are on a private node, we won't do any remote calls (just as a precaution until
        // we can configure this from config.php for the private nodes)
        if (common_config('site', 'private')) {
            return true;
        }

        // All our remote Oembed images lack a local filename property in the File object
        if (!is_null($file->filename)) {
            common_debug(sprintf('Filename of file id==%d is not null (%s), so nothing oEmbed should handle.', $file->getID(), _ve($file->filename)));
            return true;
        }

        try {
            // If we have proper oEmbed data, there should be an entry in the File_oembed
            // and File_thumbnail tables respectively. If not, we're not going to do anything.
            $thumbnail = File_thumbnail::byFile($file);
        } catch (NoResultException $e) {
            // Not Oembed data, or at least nothing we either can or want to use.
            common_debug('No oEmbed data found for file id=='.$file->getID());
            return true;
        }

        try {
            $this->storeRemoteFileThumbnail($thumbnail);
        } catch (AlreadyFulfilledException $e) {
            // aw yiss!
        } catch (Exception $e) {
            common_debug(sprintf('oEmbed encountered an exception (%s) for file id==%d: %s', get_class($e), $file->getID(), _ve($e->getMessage())));
            throw $e;
        }

        $imgPath = $thumbnail->getPath();

        return false;
    }

    /**
     * @return bool             false on no check made, provider name on success
     * @throws ServerException  if check is made but fails
     */
    protected function checkWhitelist($url)
    {
        if (!$this->check_whitelist) {
            return false;   // indicates "no check made"
        }

        $host = parse_url($url, PHP_URL_HOST);
        foreach ($this->domain_whitelist as $regex => $provider) {
            if (preg_match("/$regex/", $host)) {
                return $provider;    // we trust this source, return provider name
            }
        }

        throw new ServerException(sprintf(_('Domain not in remote thumbnail source whitelist: %s'), $host));
    }

    /**
     * Check the file size of a remote file using a HEAD request and checking
     * the content-length variable returned.  This isn't 100% foolproof but is
     * reliable enough for our purposes.
     *
     * @return string|bool the file size if it succeeds, false otherwise.
     */
    private function getRemoteFileSize($url)
    {
        try {
            if (empty($url)) {
                return false;
            }
            stream_context_set_default(array('http' => array('method' => 'HEAD')));
            $head = @get_headers($url, 1);
            if (gettype($head)=="array") {
                $head = array_change_key_case($head);
                $size = isset($head['content-length']) ? $head['content-length'] : 0;

                if (!$size) {
                    return false;
                }
            } else {
                return false;
            }
            return $size; // return formatted size
        } catch (Exception $err) {
            common_log(LOG_ERR, __CLASS__.': getRemoteFileSize on URL : '._ve($url).' threw exception: '.$err->getMessage());
            return false;
        }
    }

    /**
     * A private helper function that uses a CURL lookup to check the mime type
     * of a remote URL to see it it's an image.
     *
     * @return bool true if the remote URL is an image, or false otherwise.
     */
    private function isRemoteImage($url)
    {
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            common_log(LOG_ERR, "Invalid URL in OEmbed::isRemoteImage()");
            return false;
        }
        if ($url==null) {
            common_log(LOG_ERR, "URL not specified in OEmbed::isRemoteImage()");
            return false;
        }
        try {
            $curl = curl_init($url);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_HEADER, true);
            curl_setopt($curl, CURLOPT_NOBODY, true);
            curl_exec($curl);
            $type    = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
            if (strpos($type, 'image') !== false) {
                return true;
            } else {
                return false;
            }
        } finally {
            return false;
        }
    }

    /**
     * An internal helper function that parses the php.ini file size limit from
     * the 'human-readable' shorthand into something we can use to test against
     * in conditionals.
     *
     * FIXME: We could probably move this to a public utility library.
     *
     * @return string the php.ini upload limit in machine-readable format, or the
     * exception if it fails.
     */
    private function getPHPUploadLimit()
    {
        $size   = ini_get("upload_max_filesize");
        $suffix = substr($size, -1);
        $size   = substr($size, 0, -1);
        switch (strtoupper($suffix)) {
        case 'P':
            $size *= 1024;
            // no break
        case 'T':
            $size *= 1024;
            // no break
        case 'G':
            $size *= 1024;
            // no break
        case 'M':
            $size *= 1024;
            // no break
        case 'K':
            $size *= 1024;
            break;
        }
        return $size;
    }

    /**
     * Creates a dummy image with dimensions of original image in order to cheat
     * core GNU social file handler. Used by remove_original.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param int $width Image's width
     * @param int $height Image's height
     * @return string dummy file's filename
     */
    private function create_dummy($width, $height)
    {
        $filename = "oembed-dummy-{$width}-{$height}.png";
        $filepath = File_thumbnail::path($filename);
        if (file_exists($filepath)) {
            return $filename;
        }

        $im = @imagecreate($width, $height)
           || exit('Cannot Initialize new GD image stream');
        imagecolorallocate($im, 0, 0, 0);
        imagepng($im, File_thumbnail::path ($filename));
        imagedestroy($im);

        return $filename;
    }

    /**
     * Removes the original thumbnail and updates the entry on database to the
     * dummy object. Depends on create_dummy.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param File_thumbnail $thumbnail object containing the file thumbnail
     * @return bool true if had to remove, false if it was already all okay
     */
    private function remove_original($thumbnail)
    {
        // Do not remove when original is needed
        if ($this->keep_original ||
            (isset($config['thumbnail']) &&
             ($config['thumbnail']['width']  <= $thumbnail->width) &&
             ($config['thumbnail']['height'] <= $thumbnail->height))
           ) {
            return false;
        }

        $dummy_filename = $this->create_dummy($thumbnail->width, $thumbnail->height);
        if ($thumbnail->filename == $dummy_filename) {
            return false;
        }
        $original_filepath = File_thumbnail::path($thumbnail->filename);
        unlink($original_filepath);

        $orig = clone ($thumbnail);
        $thumbnail->filename = $dummy_filename;
        // Throws exception on failure.
        $thumbnail->updateWithKeys($orig);

        return true;
    }

    /**
     * Function to create and store a thumbnail representation of a remote image
     *
     * @param $thumbnail File_thumbnail object containing the file thumbnail
     * @return bool true if it succeeded, the exception if it fails, or false if it
     * is limited by system limits (ie the file is too large.)
     */
    protected function storeRemoteFileThumbnail(File_thumbnail $thumbnail)
    {
        if (!empty($thumbnail->filename) && file_exists($thumbnail->getPath())) {
            $this->remove_original($thumbnail);
            throw new AlreadyFulfilledException(sprintf('A thumbnail seems to already exist for remote file with id==%u', $thumbnail->file_id));
        }

        $url = $thumbnail->getUrl();
        $this->checkWhitelist($url);

        try {
            $isImage = $this->isRemoteImage($url);
            if ($isImage==true) {
                $max_size  = $this->getPHPUploadLimit();
                $file_size = $this->getRemoteFileSize($url);
                if (($file_size!=false) && ($file_size > $max_size)) {
                    common_debug("Went to store remote thumbnail of size " . $file_size . " but the upload limit is " . $max_size . " so we aborted.");
                    return false;
                }
            }
        } catch (Exception $err) {
            common_debug("Could not determine size of remote image, aborted local storage.");
            return $err;
        }

        // First we download the file to memory and test whether it's actually an image file
        // FIXME: To support remote video/whatever files, this needs reworking.
        common_debug(sprintf('Downloading remote thumbnail for file id==%u with thumbnail URL: %s', $thumbnail->file_id, $url));
        $imgData = HTTPClient::quickGet($url);
        $info = @getimagesizefromstring($imgData);
        if ($info === false) {
            throw new UnsupportedMediaException(_('Remote file format was not identified as an image.'), $url);
        } elseif (!$info[0] || !$info[1]) {
            throw new UnsupportedMediaException(_('Image file had impossible geometry (0 width or height)'));
        }

        $ext = File::guessMimeExtension($info['mime']);

        try {
            // We'll trust sha256 (File::FILEHASH_ALG) not to have collision issues any time soon :)
            $filename = sprintf('oembed-%d.%s', hash(File::FILEHASH_ALG, $imgData), $ext);
            $fullpath = File_thumbnail::path($filename);
            // Write the file to disk. Throw Exception on failure
            if (!file_exists($fullpath) && file_put_contents($fullpath, $imgData) === false) {
                throw new ServerException(_('Could not write downloaded file to disk.'));
            }
        } catch (Exception $err) {
            common_log(LOG_ERROR, "Went to write a thumbnail to disk in OembedPlugin::storeRemoteThumbnail but encountered error: {$err}");
            return $err;
        } finally {
            unset($imgData);
        }

        try {
            // Updated our database for the file record
            $orig = clone($thumbnail);
            $thumbnail->filename = $filename;
            $thumbnail->width = $info[0];    // array indexes documented on php.net:
            $thumbnail->height = $info[1];   // https://php.net/manual/en/function.getimagesize.php
            // Throws exception on failure.
            $thumbnail->updateWithKeys($orig);
        } catch (exception $err) {
            common_log(LOG_ERROR, "Went to write a thumbnail entry to the database in OembedPlugin::storeRemoteThumbnail but encountered error: ".$err);
            return $err;
        }
        return true;
    }

    /**
     * Event raised when GNU social polls the plugin for information about it.
     * Adds this plugin's version information to $versions array
     *
     * @param &$versions array inherited from parent
     * @return bool true hook value
     */
    public function onPluginVersion(array &$versions)
    {
        $versions[] = array('name' => 'Oembed',
                            'version' => self::PLUGIN_VERSION,
                            'author' => 'Mikael Nordfeldth',
                            'homepage' => 'http://gnu.io/social/',
                            'description' =>
                            // TRANS: Plugin description.
                            _m('Plugin for using and representing Oembed data.'));
        return true;
    }
}


code:
The Oembed plugin for using and representing oEmbed data.

See: http://www.oembed.com/

Installation
============
This plugin is enabled by default

Settings
========
width: Maximum width of the thumbnail in pixels.
height: Maximum height of the thumbnail in pixels.
show_html: Whether to show HTML oEmbed data.
domain_whitelist: Array of regular expressions. Always escape your dots and end your strings.
check_whitelist: Whether to check the domain_whitelist.
keep_original: Whether to keep original image or not.

Example
=======
$config['thumbnail']['width'] = 42;
$config['thumbnail']['height'] = 42;
$config['attachments']['show_html'] = true;
addPlugin('Oembed', array(
    'domain_whitelist' => array(
        '^i\d*\.ytimg\.com$' => 'YouTube',
        '^i\d*\.vimeocdn\.com$' => 'Vimeo'
    ),
    'check_whitelist' => true
));

path: plugins/Oembed/README


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * OembedPlugin implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Craig Andrews <candrews@integralblue.com>
 * @author    Mikael Nordfeldth <mmn@hethane.se>
 * @author    hannes
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

/**
 * Oembed provider implementation
 *
 * This class handles all /main/oembed(.xml|.json)/ requests.
 *
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class OembedAction extends Action
{
    protected function handle()
    {
        parent::handle();

        $url = $this->trimmed('url');
        $tls = parse_url($url, PHP_URL_SCHEME) == 'https';
        $root_url = common_root_url($tls);

        if (substr(strtolower($url), 0, mb_strlen($root_url)) !== strtolower($root_url)) {
            // TRANS: Error message displaying attachments. %s is the site's base URL.
            throw new ClientException(sprintf(_('oEmbed data will only be provided for %s URLs.'), $root_url));
        }

        $path = substr($url, strlen($root_url));

        $r = Router::get();

        // $r->map will throw ClientException 404 if it fails to find a mapping
        $proxy_args = $r->map($path);

        $oembed=array();
        $oembed['version']='1.0';
        $oembed['provider_name']=common_config('site', 'name');
        $oembed['provider_url']=common_root_url();

        switch ($proxy_args['action']) {
        case 'shownotice':
            $oembed['type']='link';
            try {
                $notice = Notice::getByID($proxy_args['notice']);
            } catch (NoResultException $e) {
                throw new ClientException($e->getMessage(), 404);
            }
            $profile = $notice->getProfile();
            $authorname = $profile->getFancyName();
            // TRANS: oEmbed title. %1$s is the author name, %2$s is the creation date.
            $oembed['title'] = sprintf(
                _('%1$s\'s status on %2$s'),
                $authorname,
                common_exact_date($notice->created)
            );
            $oembed['author_name']=$authorname;
            $oembed['author_url']=$profile->profileurl;
            $oembed['url']=$notice->getUrl();
            $oembed['html']=$notice->getRendered();

            // maybe add thumbnail
            foreach ($notice->attachments() as $attachment) {
                if (!$attachment instanceof File) {
                    common_debug('ATTACHMENTS array entry from notice id=='._ve($notice->getID()).' is something else than a File dataobject: '._ve($attachment));
                    continue;
                }
                try {
                    $thumb = $attachment->getThumbnail();
                    $thumb_url = File_thumbnail::url($thumb->filename);
                    $oembed['thumbnail_url'] = $thumb_url;
                    break;  // only first one
                } catch (UseFileAsThumbnailException $e) {
                    $oembed['thumbnail_url'] = $attachment->getUrl();
                    break;  // we're happy with that
                } catch (ServerException $e) {
                    //
                } catch (ClientException $e) {
                    //
                }
            }
            break;

        case 'attachment':
            $id = $proxy_args['attachment'];
            $attachment = File::getKV($id);
            if (empty($attachment)) {
                // TRANS: Client error displayed in oEmbed action when attachment not found.
                // TRANS: %d is an attachment ID.
                $this->clientError(sprintf(_('Attachment %s not found.'), $id), 404);
            }
            if (empty($attachment->filename) && $file_oembed = File_oembed::getKV('file_id', $attachment->id)) {
                // Proxy the existing oembed information
                $oembed['type']=$file_oembed->type;
                $oembed['provider']=$file_oembed->provider;
                $oembed['provider_url']=$file_oembed->provider_url;
                $oembed['width']=$file_oembed->width;
                $oembed['height']=$file_oembed->height;
                $oembed['html']=$file_oembed->html;
                $oembed['title']=$file_oembed->title;
                $oembed['author_name']=$file_oembed->author_name;
                $oembed['author_url']=$file_oembed->author_url;
                $oembed['url']=$file_oembed->getUrl();
            } elseif (substr($attachment->mimetype, 0, strlen('image/'))==='image/') {
                $oembed['type']='photo';
                if ($attachment->filename) {
                    $filepath = File::path($attachment->filename);
                    $gis = @getimagesize($filepath);
                    if ($gis) {
                        $oembed['width'] = $gis[0];
                        $oembed['height'] = $gis[1];
                    } else {
                        // TODO Either throw an error or find a fallback?
                    }
                }
                $oembed['url']=$attachment->getUrl();
                try {
                    $thumb = $attachment->getThumbnail();
                    $oembed['thumbnail_url'] = $thumb->getUrl();
                    $oembed['thumbnail_width'] = $thumb->width;
                    $oembed['thumbnail_height'] = $thumb->height;
                    unset($thumb);
                } catch (UnsupportedMediaException $e) {
                    // No thumbnail data available
                }
            } else {
                $oembed['type']='link';
                $oembed['url']=common_local_url(
                    'attachment',
                    array('attachment' => $attachment->id)
                );
            }
            if ($attachment->title) {
                $oembed['title']=$attachment->title;
            }
            break;
        default:
            // TRANS: Server error displayed in oEmbed request when a path is not supported.
            // TRANS: %s is a path.
            $this->serverError(sprintf(_('"%s" not supported for oembed requests.'), $path), 501);
        }

        switch ($this->trimmed('format')) {
        case 'xml':
            $this->init_document('xml');
            $this->elementStart('oembed');
            foreach (array(
                        'version', 'type', 'provider_name',
                        'provider_url', 'title', 'author_name',
                        'author_url', 'url', 'html', 'width',
                        'height', 'cache_age', 'thumbnail_url',
                        'thumbnail_width', 'thumbnail_height',
                    ) as $key) {
                if (isset($oembed[$key]) && $oembed[$key]!='') {
                    $this->element($key, null, $oembed[$key]);
                }
            }
            $this->elementEnd('oembed');
            $this->end_document('xml');
            break;

        case 'json':
        case null:
            $this->init_document('json');
            $this->raw(json_encode($oembed));
            $this->end_document('json');
            break;
        default:
            // TRANS: Error message displaying attachments. %s is a raw MIME type (eg 'image/png')
            $this->serverError(sprintf(_('Content type %s not supported.'), $apidata['content-type']), 501);
        }
    }

    public function init_document($type)
    {
        switch ($type) {
        case 'xml':
            header('Content-Type: application/xml; charset=utf-8');
            $this->startXML();
            break;
        case 'json':
            header('Content-Type: application/json; charset=utf-8');

            // Check for JSONP callback
            $callback = $this->arg('callback');
            if ($callback) {
                print $callback . '(';
            }
            break;
        default:
            // TRANS: Server error displayed in oEmbed action when request specifies an unsupported data format.
            $this->serverError(_('Not a supported data format.'), 501);
            break;
        }
    }

    public function end_document($type)
    {
        switch ($type) {
        case 'xml':
            $this->endXML();
            break;
        case 'json':
            // Check for JSONP callback
            $callback = $this->arg('callback');
            if ($callback) {
                print ')';
            }
            break;
        default:
            // TRANS: Server error displayed in oEmbed action when request specifies an unsupported data format.
            $this->serverError(_('Not a supported data format.'), 501);
            break;
        }
        return;
    }

    /**
     * Is this action read-only?
     *
     * @param array $args other arguments
     *
     * @return boolean is read only action?
     */
    public function isReadOnly($args)
    {
        return true;
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * OembedPlugin implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Stephen Paul Weber
 * @author    Mikael Nordfeldth
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

/**
 * Table Definition for file_oembed
 *
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class File_oembed extends Managed_DataObject
{
    public $__table = 'file_oembed';                     // table name
    public $file_id;                         // int(4)  primary_key not_null
    public $version;                         // varchar(20)
    public $type;                            // varchar(20)
    public $mimetype;                        // varchar(50)
    public $provider;                        // varchar(50)
    public $provider_url;                    // varchar(191)   not 255 because utf8mb4 takes more space
    public $width;                           // int(4)
    public $height;                          // int(4)
    public $html;                            // text()
    public $title;                           // varchar(191)   not 255 because utf8mb4 takes more space
    public $author_name;                     // varchar(50)
    public $author_url;                      // varchar(191)   not 255 because utf8mb4 takes more space
    public $url;                             // varchar(191)   not 255 because utf8mb4 takes more space
    public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'file_id' => array('type' => 'int', 'not null' => true, 'description' => 'oEmbed for that URL/file'),
                'version' => array('type' => 'varchar', 'length' => 20, 'description' => 'oEmbed spec. version'),
                'type' => array('type' => 'varchar', 'length' => 20, 'description' => 'oEmbed type: photo, video, link, rich'),
                'mimetype' => array('type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'),
                'provider' => array('type' => 'text', 'description' => 'name of this oEmbed provider'),
                'provider_url' => array('type' => 'text', 'description' => 'URL of this oEmbed provider'),
                'width' => array('type' => 'int', 'description' => 'width of oEmbed resource when available'),
                'height' => array('type' => 'int', 'description' => 'height of oEmbed resource when available'),
                'html' => array('type' => 'text', 'description' => 'html representation of this oEmbed resource when applicable'),
                'title' => array('type' => 'text', 'description' => 'title of oEmbed resource when available'),
                'author_name' => array('type' => 'text', 'description' => 'author name for this oEmbed resource'),
                'author_url' => array('type' => 'text', 'description' => 'author URL for this oEmbed resource'),
                'url' => array('type' => 'text', 'description' => 'URL for this oEmbed resource when applicable (photo, link)'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('file_id'),
            'foreign keys' => array(
                'file_oembed_file_id_fkey' => array('file', array('file_id' => 'id')),
            ),
        );
    }

    public static function _getOembed($url)
    {
        try {
            return oEmbedHelper::getObject($url);
        } catch (Exception $e) {
            common_log(LOG_INFO, "Error during oembed lookup for $url - " . $e->getMessage());
            return false;
        }
    }

    /**
     * Fetch an entry by using a File's id
     */
    public static function getByFile(File $file)
    {
        $fo = new File_oembed();
        $fo->file_id = $file->id;
        if (!$fo->find(true)) {
            throw new NoResultException($fo);
        }
        return $fo;
    }

    public function getUrl()
    {
        return $this->url;
    }

    /**
     * Save embedding info for a new file.
     *
     * @param object $data Services_oEmbed_Object_*
     * @param int $file_id
     */
    public static function saveNew($data, $file_id)
    {
        $file_oembed = new File_oembed;
        $file_oembed->file_id = $file_id;
        if (!isset($data->version)) {
            common_debug('DEBUGGING oEmbed: data->version undefined in variable $data: '.var_export($data, true));
        }
        $file_oembed->version = $data->version;
        $file_oembed->type = $data->type;
        if (!empty($data->provider_name)) {
            $file_oembed->provider = $data->provider_name;
        }
        if (!empty($data->provider)) {
            $file_oembed->provider = $data->provider;
        }
        if (!empty($data->provider_url)) {
            $file_oembed->provider_url = $data->provider_url;
        }
        if (!empty($data->width)) {
            $file_oembed->width = intval($data->width);
        }
        if (!empty($data->height)) {
            $file_oembed->height = intval($data->height);
        }
        if (!empty($data->html)) {
            $file_oembed->html = $data->html;
        }
        if (!empty($data->title)) {
            $file_oembed->title = $data->title;
        }
        if (!empty($data->author_name)) {
            $file_oembed->author_name = $data->author_name;
        }
        if (!empty($data->author_url)) {
            $file_oembed->author_url = $data->author_url;
        }
        if (!empty($data->url)) {
            $file_oembed->url = $data->url;
            $given_url = File_redirection::_canonUrl($file_oembed->url);
            if (! empty($given_url)) {
                try {
                    $file = File::getByUrl($given_url);
                    $file_oembed->mimetype = $file->mimetype;
                } catch (NoResultException $e) {
                    // File_redirection::where argument 'discover' is false to avoid loops
                    $redir = File_redirection::where($given_url, false);
                    if (!empty($redir->file_id)) {
                        $file_id = $redir->file_id;
                    }
                }
            }
        }
        $result = $file_oembed->insert();
        if ($result === false) {
            throw new ServerException('Failed to insert File_oembed data into database!');
        }
        if (!empty($data->thumbnail_url) || ($data->type == 'photo')) {
            $ft = File_thumbnail::getKV('file_id', $file_id);
            if ($ft instanceof File_thumbnail) {
                common_log(
                    LOG_WARNING,
                    "Strangely, a File_thumbnail object exists for new file $file_id",
                    __FILE__
                );
            } else {
                File_thumbnail::saveNew($data, $file_id);
            }
        }
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * OembedPlugin implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Mikael Nordfeldth
 * @author    hannes
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

/**
 * Utility class to wrap basic oEmbed lookups.
 *
 * 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'.
 *
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class oEmbedHelper
{
    protected static $apiMap = array(
        'flickr.com' => 'https://www.flickr.com/services/oembed/',
        'youtube.com' => 'https://www.youtube.com/oembed',
        'viddler.com' => 'http://lab.viddler.com/services/oembed/',
        'revision3.com' => 'https://revision3.com/api/oembed/',
        'vimeo.com' => 'https://vimeo.com/api/oembed.json',
    );

    /**
     * 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 noembed.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, 'Checking for remote URL metadata for ' . $url);

        // TODO: Make this class something like UrlMetadata, or use a dataobject?
        $metadata = new stdClass();

        if (Event::handle('GetRemoteUrlMetadata', array($url, &$metadata))) {
            // If that event didn't return anything, try downloading the body and parse it

            // don't use quickGet since we want to check Content-Type header for utf-8
            $client = new HTTPClient();
            $response = $client->get($url);
            if (!$response->isOk()) {
                // TRANS: Exception. %s is the URL we tried to GET.
                throw new Exception(sprintf(_m('Could not GET URL %s.'), $url), $response->getStatus());
            }
            $body = $response->getBody();

            // DOMDocument::loadHTML may throw warnings on unrecognized elements,
            // and notices on unrecognized namespaces.
            $old = error_reporting(error_reporting() & ~(E_WARNING | E_NOTICE));

            // DOMDocument assumes ISO-8859-1 per HTML spec
            // use UTF-8 if we find any evidence of that encoding
            $utf8_evidence = false;
            $unicode_check_dom = new DOMDocument();
            $ok = $unicode_check_dom->loadHTML($body);
            if (!$ok) {
                throw new oEmbedHelper_BadHtmlException();
            }
            $metaNodes = $unicode_check_dom->getElementsByTagName('meta');
            foreach ($metaNodes as $metaNode) {
                // case in-sensitive since Content-type and utf-8 can be written in many ways
                if (stristr($metaNode->getAttribute('http-equiv'), 'content-type')
                && stristr($metaNode->getAttribute('content'), 'utf-8')) {
                    $utf8_evidence = true;
                    break;
                } elseif (stristr($metaNode->getAttribute('charset'), 'utf-8')) {
                    $utf8_evidence = true;
                    break;
                }
            }
            unset($unicode_check_dom);

            // The Content-Type HTTP response header overrides encoding metatags in DOM
            if (stristr($response->getHeader('Content-Type'), 'utf-8')) {
                $utf8_evidence = true;
            }

            // add utf-8 encoding prolog if we have reason to believe this is utf-8 content
            // DOMDocument('1.0', 'UTF-8') does not work!
            $utf8_tag = $utf8_evidence ? '<?xml encoding="utf-8" ?>' : '';

            $dom = new DOMDocument();
            $ok = $dom->loadHTML($utf8_tag.$body);
            unset($body);   // storing the DOM in memory is enough...
            error_reporting($old);

            if (!$ok) {
                throw new oEmbedHelper_BadHtmlException();
            }

            Event::handle('GetRemoteUrlMetadataFromDom', array($url, $dom, &$metadata));
        }

        return self::normalize($metadata);
    }

    /**
     * Partially ripped from OStatus' FeedDiscovery class.
     *
     * @param string $url source URL, used to resolve relative links
     * @param string $body HTML body text
     * @return mixed string with URL or false if no target found
     */
    public static function oEmbedEndpointFromHTML(DOMDocument $dom)
    {
        // Ok... now on to the links!
        $feeds = array(
            'application/json+oembed' => false,
        );

        $nodes = $dom->getElementsByTagName('link');
        for ($i = 0; $i < $nodes->length; $i++) {
            $node = $nodes->item($i);
            if ($node->hasAttributes()) {
                $rel = $node->attributes->getNamedItem('rel');
                $type = $node->attributes->getNamedItem('type');
                $href = $node->attributes->getNamedItem('href');
                if ($rel && $type && $href) {
                    $rel = array_filter(explode(" ", $rel->value));
                    $type = trim($type->value);
                    $href = trim($href->value);

                    if (in_array('alternate', $rel) && array_key_exists($type, $feeds) && empty($feeds[$type])) {
                        // Save the first feed found of each type...
                        $feeds[$type] = $href;
                    }
                }
            }
        }

        // Return the highest-priority feed found
        foreach ($feeds as $type => $url) {
            if ($url) {
                return $url;
            }
        }

        throw new oEmbedHelper_DiscoveryException();
    }

    /**
     * Actually do an oEmbed lookup to a particular API endpoint.
     *
     * @param string $api oEmbed API endpoint URL
     * @param string $url target URL to look up info about
     * @param array $params
     * @return object
     */
    public static function getOembedFrom($api, $url, $params=array())
    {
        if (empty($api)) {
            // TRANS: Server exception thrown in oEmbed action if no API endpoint is available.
            throw new ServerException(_('No oEmbed API endpoint available.'));
        }
        $params['url'] = $url;
        $params['format'] = 'json';
        $key=common_config('oembed', 'apikey');
        if (isset($key)) {
            $params['key'] = common_config('oembed', 'apikey');
        }

        $oembed_data = HTTPClient::quickGetJson($api, $params);
        if (isset($oembed_data->html)) {
            $oembed_data->html = common_purify($oembed_data->html);
        }

        return $oembed_data;
    }

    /**
     * Normalize oEmbed format.
     *
     * @param object $orig
     * @return object
     */
    public static function normalize(stdClass $data)
    {
        if (empty($data->type)) {
            throw new Exception('Invalid oEmbed data: no type field.');
        }
        if ($data->type == 'image') {
            // YFrog does this.
            $data->type = 'photo';
        }

        if (isset($data->thumbnail_url)) {
            if (!isset($data->thumbnail_width)) {
                // !?!?!
                $data->thumbnail_width = common_config('thumbnail', 'width');
                $data->thumbnail_height = common_config('thumbnail', 'height');
            }
        }

        return $data;
    }
}

class oEmbedHelper_Exception extends Exception
{
    public function __construct($message = "", $code = 0, $previous = null)
    {
        parent::__construct($message, $code);
    }
}

class oEmbedHelper_BadHtmlException extends oEmbedHelper_Exception
{
    public function __construct($previous=null)
    {
        return parent::__construct('Bad HTML in discovery data.', 0, $previous);
    }
}

class oEmbedHelper_DiscoveryException extends oEmbedHelper_Exception
{
    public function __construct($previous=null)
    {
        return parent::__construct('No oEmbed discovery data.', 0, $previous);
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * OembedPlugin implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Mikael Nordfeldth
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

/**
 * Utility class to get OpenGraph data from HTML DOMs etc.
 *
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class OpenGraphHelper
{
    const KEY_REGEX = '/^og\:(\w+(?:\:\w+)?)/';
    protected static $property_map = [
                            'site_name'      => 'provider_name',
                            'title'          => 'title',
                            'description'    => 'html',
                            'type'           => 'type',
                            'url'            => 'url',
                            'image'          => 'thumbnail_url',
                            'image:height'   => 'thumbnail_height',
                            'image:width'    => 'thumbnail_width',
                            ];

    // This regex map has:    /pattern(match)/ => matchindex | string
    protected static $type_regex_map = [
                            '/^(video)/'   => 1,
                            '/^image/'     => 'photo',
                            ];

    public static function ogFromHtml(DOMDocument $dom)
    {
        $obj = new stdClass();
        $obj->version = '1.0';  // fake it til u make it

        $nodes = $dom->getElementsByTagName('meta');
        for ($i = 0; $i < $nodes->length; $i++) {
            $node = $nodes->item($i);
            if (!$node->hasAttributes()) {
                continue;
            }
            $property = $node->attributes->getNamedItem('property');
            $matches = array();
            if ($property === null || !preg_match(self::KEY_REGEX, $property->value, $matches)) {
                // not property="og:something"
                continue;
            }
            if (!isset(self::$property_map[$matches[1]])) {
                // unknown metadata property, nothing we would care about anyway
                continue;
            }

            $prop = self::$property_map[$matches[1]];
            $obj->{$prop} = $node->attributes->getNamedItem('content')->value;
            // I don't care right now if they're empty
        }
        if (isset($obj->type)) {
            // Loop through each known OpenGraph type where we have a match in oEmbed
            foreach (self::$type_regex_map as $pattern=>$replacement) {
                $matches = array();
                if (preg_match($pattern, $obj->type, $matches)) {
                    $obj->type = is_int($replacement)
                                    ? $matches[$replacement]
                                    : $replacement;
                    break;
                }
            }
            // If it's not known to our type map, we just pass it through in hopes of it getting handled anyway
        } elseif (isset($obj->url)) {
            // If no type is set but we have a URL, let's set type=link
            $obj->type = 'link';
        }
        return $obj;
    }
}


code:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-10 00:39+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:721
msgid "Plugin for using and representing Oembed data."
msgstr ""

#. TRANS: Exception. %s is the URL we tried to GET.
#: lib/oembedhelper.php:93
#, php-format
msgid "Could not GET URL %s."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Afrikaans (http://www.transifex.com/gnu-social/gnu-social/language/af/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: af\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Arabic (http://www.transifex.com/gnu-social/gnu-social/language/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Arabic (Egypt) (http://www.transifex.com/gnu-social/gnu-social/language/ar_EG/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ar_EG\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Asturian (http://www.transifex.com/gnu-social/gnu-social/language/ast/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ast\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Belarusian (Tarask) (http://www.transifex.com/gnu-social/gnu-social/language/be@tarask/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: be@tarask\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Bulgarian (http://www.transifex.com/gnu-social/gnu-social/language/bg/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: bg\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Bengali (India) (http://www.transifex.com/gnu-social/gnu-social/language/bn_IN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: bn_IN\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Breton (http://www.transifex.com/gnu-social/gnu-social/language/br/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: br\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Catalan (http://www.transifex.com/gnu-social/gnu-social/language/ca/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ca\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Czech (http://www.transifex.com/gnu-social/gnu-social/language/cs/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: cs\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Danish (http://www.transifex.com/gnu-social/gnu-social/language/da/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: da\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: German (http://www.transifex.com/gnu-social/gnu-social/language/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-07 09:29+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Greek (http://www.transifex.com/gnu-social/gnu-social/language/el/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: el\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
# Luke Hollins <luke@farcry.ca>, 2015
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-03-07 17:25+0000\n"
"Last-Translator: Luke Hollins <luke@farcry.ca>\n"
"Language-Team: English (United Kingdom) (http://www.transifex.com/gnu-social/gnu-social/language/en_GB/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_GB\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: OembedPlugin.php:190
msgid "Plugin for using and representing Oembed data."
msgstr "Plugin for using and representing Oembed data."