From 1776c90cb98d5ca738db143703fb998612ada31e Mon Sep 17 00:00:00 2001
From: Mikael Nordfeldth <mmn@hethane.se>
Date: Tue, 6 May 2014 23:00:30 +0200
Subject: [PATCH] Moved oEmbed stuff out to a plugin (Oembed).

---
 CONFIGURE                                     |  17 --
 actions/attachment.php                        |  22 --
 actions/oembed.php                            | 244 ------------------
 actions/shownotice.php                        |  15 --
 classes/File.php                              |  90 ++-----
 classes/File_redirection.php                  |   3 -
 db/core.php                                   |   1 -
 lib/action.php                                |   5 +
 lib/attachmentlist.php                        |  79 +-----
 lib/default.php                               |   1 +
 lib/router.php                                |   3 -
 plugins/Bookmark/README                       |   1 +
 plugins/LinkPreview/README                    |   1 +
 plugins/Oembed/CONFIGURE                      |  18 ++
 plugins/Oembed/OembedPlugin.php               | 187 ++++++++++++++
 plugins/Oembed/README                         |   1 +
 plugins/Oembed/actions/oembed.php             | 236 +++++++++++++++++
 .../Oembed/classes}/File_oembed.php           |   0
 {lib => plugins/Oembed/lib}/oembedhelper.php  |   0
 .../Oembed/scripts}/fixup_files.php           |   9 +-
 .../Oembed/tests}/oEmbedTest.php              |   2 +-
 21 files changed, 481 insertions(+), 454 deletions(-)
 delete mode 100644 actions/oembed.php
 create mode 100644 plugins/Bookmark/README
 create mode 100644 plugins/LinkPreview/README
 create mode 100644 plugins/Oembed/CONFIGURE
 create mode 100644 plugins/Oembed/OembedPlugin.php
 create mode 100644 plugins/Oembed/README
 create mode 100644 plugins/Oembed/actions/oembed.php
 rename {classes => plugins/Oembed/classes}/File_oembed.php (100%)
 rename {lib => plugins/Oembed/lib}/oembedhelper.php (100%)
 rename {scripts => plugins/Oembed/scripts}/fixup_files.php (89%)
 rename {tests => plugins/Oembed/tests}/oEmbedTest.php (98%)

diff --git a/CONFIGURE b/CONFIGURE
index 141c475563..bec9e90a6f 100644
--- a/CONFIGURE
+++ b/CONFIGURE
@@ -592,23 +592,6 @@ desclimit: maximum number of characters to allow in group descriptions.
 addtag: Whether to add a tag for the group nickname for every group post 
 	(pre-1.0.x behaviour). Defaults to false.
 
-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.
-
 search
 ------
 
diff --git a/actions/attachment.php b/actions/attachment.php
index 30499c15f4..d9cacb1193 100644
--- a/actions/attachment.php
+++ b/actions/attachment.php
@@ -96,28 +96,6 @@ class AttachmentAction extends Action
         return $a->title();
     }
 
-    function extraHead()
-    {
-        $this->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' => $this->attachment->id)))),
-            'title'=>'oEmbed'),null);
-        $this->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' => $this->attachment->id)))),
-            'title'=>'oEmbed'),null);
-    }
-
     /**
      * Handle input
      *
diff --git a/actions/oembed.php b/actions/oembed.php
deleted file mode 100644
index da0352edf8..0000000000
--- a/actions/oembed.php
+++ /dev/null
@@ -1,244 +0,0 @@
-<?php
-/**
- * StatusNet, the distributed open-source microblogging tool
- *
- * StatusNet-only extensions to the Twitter-like API
- *
- * PHP version 5
- *
- * LICENCE: 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/>.
- *
- * @category  Twitter
- * @package   StatusNet
- * @author    Evan Prodromou <evan@status.net>
- * @copyright 2008 StatusNet, Inc.
- * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- * @link      http://status.net/
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) {
-    exit(1);
-}
-
-/**
- * Oembed provider implementation
- *
- * This class handles all /main/oembed(.xml|.json)/ requests.
- *
- * @category  oEmbed
- * @package   StatusNet
- * @author    Craig Andrews <candrews@integralblue.com>
- * @copyright 2008 StatusNet, Inc.
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- * @link      http://status.net/
- */
-
-class OembedAction extends Action
-{
-
-    function handle($args)
-    {
-        common_debug("in oembed api action");
-
-        $url = $args['url'];
-        if( substr(strtolower($url),0,strlen(common_root_url())) == strtolower(common_root_url()) ){
-            $path = substr($url,strlen(common_root_url()));
-
-            $r = Router::get();
-
-            $proxy_args = $r->map($path);
-
-            if (!$proxy_args) {
-                // TRANS: Server error displayed in oEmbed action when path not found.
-                // TRANS: %s is a path.
-                $this->serverError(sprintf(_('"%s" not found.'),$path), 404);
-            }
-            $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';
-                    $id = $proxy_args['notice'];
-                    $notice = Notice::getKV($id);
-                    if(empty($notice)){
-                        // TRANS: Server error displayed in oEmbed action when notice not found.
-                        // TRANS: %s is a notice.
-                        $this->serverError(sprintf(_("Notice %s not found."),$id), 404);
-                    }
-                    $profile = $notice->getProfile();
-                    if (empty($profile)) {
-                        // TRANS: Server error displayed in oEmbed action when notice has not profile.
-                        $this->serverError(_('Notice has no profile.'), 500);
-                    }
-                    $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->rendered;
-                    break;
-                case 'attachment':
-                    $id = $proxy_args['attachment'];
-                    $attachment = File::getKV($id);
-                    if(empty($attachment)){
-                        // TRANS: Server error displayed in oEmbed action when attachment not found.
-                        // TRANS: %d is an attachment ID.
-                        $this->serverError(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();
-                    }else if(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($args['format']){
-                case 'xml':
-                    $this->init_document('xml');
-                    $this->elementStart('oembed');
-                    $this->element('version',null,$oembed['version']);
-                    $this->element('type',null,$oembed['type']);
-                    if($oembed['provider_name']) $this->element('provider_name',null,$oembed['provider_name']);
-                    if($oembed['provider_url']) $this->element('provider_url',null,$oembed['provider_url']);
-                    if($oembed['title']) $this->element('title',null,$oembed['title']);
-                    if($oembed['author_name']) $this->element('author_name',null,$oembed['author_name']);
-                    if($oembed['author_url']) $this->element('author_url',null,$oembed['author_url']);
-                    if($oembed['url']) $this->element('url',null,$oembed['url']);
-                    if($oembed['html']) $this->element('html',null,$oembed['html']);
-                    if($oembed['width']) $this->element('width',null,$oembed['width']);
-                    if($oembed['height']) $this->element('height',null,$oembed['height']);
-                    if($oembed['cache_age']) $this->element('cache_age',null,$oembed['cache_age']);
-                    if($oembed['thumbnail_url']) $this->element('thumbnail_url',null,$oembed['thumbnail_url']);
-                    if($oembed['thumbnail_width']) $this->element('thumbnail_width',null,$oembed['thumbnail_width']);
-                    if($oembed['thumbnail_height']) $this->element('thumbnail_height',null,$oembed['thumbnail_height']);
-
-                    $this->elementEnd('oembed');
-                    $this->end_document('xml');
-                    break;
-                case 'json': case '':
-                    $this->init_document('json');
-                    print(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);
-            }
-        }else{
-            // TRANS: Error message displaying attachments. %s is the site's base URL.
-            $this->serverError(sprintf(_('Only %s URLs over plain HTTP please.'), common_root_url()), 404);
-        }
-    }
-
-    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;
-        }
-    }
-
-    function end_document($type='xml')
-    {
-        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?
-     */
-    function isReadOnly($args)
-    {
-        return true;
-    }
-}
diff --git a/actions/shownotice.php b/actions/shownotice.php
index ff39080eeb..28cb68c121 100644
--- a/actions/shownotice.php
+++ b/actions/shownotice.php
@@ -282,21 +282,6 @@ class ShownoticeAction extends Action
                                          'content' => $id->toString()));
         }
 
-        $this->element('link',array('rel'=>'alternate',
-            'type'=>'application/json+oembed',
-            'href'=>common_local_url(
-                'oembed',
-                array(),
-                array('format'=>'json','url'=>$this->notice->getUrl())),
-            'title'=>'oEmbed'),null);
-        $this->element('link',array('rel'=>'alternate',
-            'type'=>'text/xml+oembed',
-            'href'=>common_local_url(
-                'oembed',
-                array(),
-                array('format'=>'xml','url'=>$this->notice->getUrl())),
-            'title'=>'oEmbed'),null);
-
         // Extras to aid in sharing notices to Facebook
         $avatarUrl = $this->profile->avatarUrl(AVATAR_PROFILE_SIZE);
         $this->element('meta', array('property' => 'og:image',
diff --git a/classes/File.php b/classes/File.php
index 29dc7b5492..69aee7fcee 100644
--- a/classes/File.php
+++ b/classes/File.php
@@ -75,62 +75,26 @@ class File extends Managed_DataObject
         // I don't know why we have to keep doing this but I'm adding this last check to avoid
         // uniqueness bugs.
 
-        $x = File::getKV('url', $given_url);
+        $file = File::getKV('url', $given_url);
         
-        if (!$x instanceof File) {
-            $x = new File;
-            $x->url = $given_url;
-            if (!empty($redir_data['protected'])) $x->protected = $redir_data['protected'];
-            if (!empty($redir_data['title'])) $x->title = $redir_data['title'];
-            if (!empty($redir_data['type'])) $x->mimetype = $redir_data['type'];
-            if (!empty($redir_data['size'])) $x->size = intval($redir_data['size']);
-            if (isset($redir_data['time']) && $redir_data['time'] > 0) $x->date = intval($redir_data['time']);
-            $file_id = $x->insert();
+        if (!$file instanceof File) {
+            $file = new File;
+            $file->url = $given_url;
+            if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected'];
+            if (!empty($redir_data['title'])) $file->title = $redir_data['title'];
+            if (!empty($redir_data['type'])) $file->mimetype = $redir_data['type'];
+            if (!empty($redir_data['size'])) $file->size = intval($redir_data['size']);
+            if (isset($redir_data['time']) && $redir_data['time'] > 0) $file->date = intval($redir_data['time']);
+            $file_id = $file->insert();
         }
 
-        $x->saveOembed($redir_data, $given_url);
-        return $x;
-    }
-
-    /**
-     * Save embedding information for this file, if applicable.
-     *
-     * Normally this won't need to be called manually, as File::saveNew()
-     * takes care of it.
-     *
-     * @param array $redir_data lookup data eg from File_redirection::where()
-     * @param string $given_url
-     * @return boolean success
-     */
-    public function saveOembed(array $redir_data, $given_url)
-    {
-        if (isset($redir_data['type'])
-                && (('text/html' === substr($redir_data['type'], 0, 9)
-                || 'application/xhtml+xml' === substr($redir_data['type'], 0, 21)))) {
-            try {
-                $oembed_data = File_oembed::_getOembed($given_url);
-            } catch (Exception $e) {
-                return false;
-            }
-            if ($oembed_data === false) {
-                return false;
-            }
-            $fo = File_oembed::getKV('file_id', $this->id);
-
-            if ($fo instanceof File_oembed) {
-                common_log(LOG_WARNING, "Strangely, a File_oembed object exists for new file $file_id", __FILE__);
-            } else {
-                File_oembed::saveNew($oembed_data, $this->id);
-                return true;
-            }
-        }
-        return false;
+        Event::handle('EndFileSaveNew', array($file, $redir_data, $given_url));
+        return $file;
     }
 
     /**
      * Go look at a URL and possibly save data about it if it's new:
      * - follow redirect chains and store them in file_redirection
-     * - look up oEmbed data and save it in file_oembed
      * - if a thumbnail is available, save it in file_thumbnail
      * - save file record with basic info
      * - optionally save a file_to_post record
@@ -384,7 +348,7 @@ class File extends Managed_DataObject
         $enclosure->size=$this->size;
         $enclosure->mimetype=$this->mimetype;
 
-        if(! isset($this->filename)){
+        if (!isset($this->filename)) {
             $notEnclosureMimeTypes = array(null,'text/html','application/xhtml+xml');
             $mimetype = $this->mimetype;
             if($mimetype != null){
@@ -394,32 +358,8 @@ class File extends Managed_DataObject
             if($semicolon){
                 $mimetype = substr($mimetype,0,$semicolon);
             }
-            if(in_array($mimetype,$notEnclosureMimeTypes)){
-                // 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',$this->id);
-                if($oembed && in_array($oembed->type, array('photo', 'video'))){
-                    $mimetype = strtolower($oembed->mimetype);
-                    $semicolon = strpos($mimetype,';');
-                    if($semicolon){
-                        $mimetype = substr($mimetype,0,$semicolon);
-                    }
-                    // @fixme uncertain if this is right.
-                    // we want to expose things like YouTube videos as
-                    // viewable attachments, but don't expose them as
-                    // downloadable enclosures.....?
-                    //if (in_array($mimetype, $notEnclosureMimeTypes)) {
-                    //    return false;
-                    //} else {
-                        if($oembed->mimetype) $enclosure->mimetype=$oembed->mimetype;
-                        if($oembed->url) $enclosure->url=$oembed->url;
-                        if($oembed->title) $enclosure->title=$oembed->title;
-                        if($oembed->modified) $enclosure->modified=$oembed->modified;
-                        unset($oembed->size);
-                    //}
-                } else {
-                    return false;
-                }
+            if (in_array($mimetype, $notEnclosureMimeTypes)) {
+                Event::handle('FileEnclosureMetadata', array($file, &$enclosure));
             }
         }
         return $enclosure;
diff --git a/classes/File_redirection.php b/classes/File_redirection.php
index 42021668d9..0bcccc6cff 100644
--- a/classes/File_redirection.php
+++ b/classes/File_redirection.php
@@ -257,9 +257,6 @@ class File_redirection extends Managed_DataObject
                     // Save file and embedding data about it!
                     $file = File::saveNew($redir_data, $long_url);
                     $file_id = $file->id;
-                    if (!empty($redir_data['oembed']['json'])) {
-                        File_oembed::saveNew($redir_data['oembed']['json'], $file_id);
-                    }
                 } else if (is_string($redir_data)) {
                     // The file is a known redirect target.
                     $file = File::getKV('url', $redir_data);
diff --git a/db/core.php b/db/core.php
index d435c03cd6..61f8228cb5 100644
--- a/db/core.php
+++ b/db/core.php
@@ -67,7 +67,6 @@ $classes = array('Schema_version',
                  'Group_inbox',
                  'Group_member',
                  'File',
-                 'File_oembed',
                  'File_redirection',
                  'File_thumbnail',
                  'File_to_post',
diff --git a/lib/action.php b/lib/action.php
index e219847c70..4d917dc69e 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -166,6 +166,11 @@ class Action extends HTMLOutputter // lawsuit
         return $this->scoped;
     }
 
+    public function getActionName()
+    {
+        return $this->action;
+    }
+
     /**
      * Show page, a template method.
      *
diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php
index 7f356f9527..66b03182b3 100644
--- a/lib/attachmentlist.php
+++ b/lib/attachmentlist.php
@@ -134,8 +134,6 @@ class AttachmentListItem extends Widget
 
     var $attachment = null;
 
-    var $oembed = null;
-
     /**
      * @param File $attachment the attachment we will display
      */
@@ -143,21 +141,10 @@ class AttachmentListItem extends Widget
     {
         parent::__construct($out);
         $this->attachment  = $attachment;
-        $this->oembed = File_oembed::getKV('file_id', $this->attachment->id);
     }
 
     function title() {
-        if (empty($this->attachment->title)) {
-            if (empty($this->oembed->title)) {
-                $title = $this->attachment->filename;
-            } else {
-                $title = $this->oembed->title;
-            }
-        } else {
-            $title = $this->attachment->title;
-        }
-
-        return $title;
+        return $this->attachment->title ?: $this->attachment->filename;
     }
 
     function linkTitle() {
@@ -238,40 +225,19 @@ class AttachmentListItem extends Widget
 class Attachment extends AttachmentListItem
 {
     function showLink() {
-        $this->out->elementStart('div', array('id' => 'attachment_view',
-                                              'class' => 'hentry'));
-        $this->out->elementStart('div', 'entry-title');
-        $this->out->element('a', $this->linkAttr(), $this->linkTitle());
-        $this->out->elementEnd('div');
+        if (Event::handle('StartShowAttachmentLink', array($this->out, $this->attachment))) {
+            $this->out->elementStart('div', array('id' => 'attachment_view',
+                                                  'class' => 'hentry'));
+            $this->out->elementStart('div', 'entry-title');
+            $this->out->element('a', $this->linkAttr(), $this->linkTitle());
+            $this->out->elementEnd('div');
 
-        $this->out->elementStart('div', 'entry-content');
-        $this->showRepresentation();
-        $this->out->elementEnd('div');
-
-        if (!empty($this->oembed->author_name) || !empty($this->oembed->provider)) {
-            $this->out->elementStart('div', array('id' => 'oembed_info',
-                                                  'class' => 'entry-content'));
-            if (!empty($this->oembed->author_name)) {
-                $this->out->elementStart('div', 'fn vcard author');
-                if (empty($this->oembed->author_url)) {
-                    $this->out->text($this->oembed->author_name);
-                } else {
-                    $this->out->element('a', array('href' => $this->oembed->author_url,
-                                                   'class' => 'url'), $this->oembed->author_name);
-                }
-            }
-            if (!empty($this->oembed->provider)) {
-                $this->out->elementStart('div', 'fn vcard');
-                if (empty($this->oembed->provider_url)) {
-                    $this->out->text($this->oembed->provider);
-                } else {
-                    $this->out->element('a', array('href' => $this->oembed->provider_url,
-                                                   'class' => 'url'), $this->oembed->provider);
-                }
-            }
+            $this->out->elementStart('div', 'entry-content');
+            $this->showRepresentation();
+            $this->out->elementEnd('div');
+            Event::handle('EndShowAttachmentLink', array($this->out, $this->attachment));
             $this->out->elementEnd('div');
         }
-        $this->out->elementEnd('div');
     }
 
     function show() {
@@ -288,28 +254,7 @@ class Attachment extends AttachmentListItem
 
     function showRepresentation() {
         if (Event::handle('StartShowAttachmentRepresentation', array($this->out, $this->attachment))) {
-            if (!empty($this->oembed->type)) {
-                switch ($this->oembed->type) {
-                case 'rich':
-                case 'video':
-                case 'link':
-                    if (!empty($this->oembed->html)) {
-                        require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
-                        $config = array(
-                            'safe'=>1,
-                            'elements'=>'*+object+embed');
-                        $this->out->raw(htmLawed($this->oembed->html,$config));
-                    }
-                    break;
-
-                case 'photo':
-                    $this->out->element('img', array('src' => $this->oembed->url, 'width' => $this->oembed->width, 'height' => $this->oembed->height, 'alt' => 'alt'));
-                    break;
-
-                default:
-                    Event::handle('ShowUnsupportedAttachmentRepresentation', array($this->out, $this->attachment));
-                }
-            } elseif (!empty($this->attachment->mimetype)) {
+            if (!empty($this->attachment->mimetype)) {
                 switch ($this->attachment->mimetype) {
                 case 'image/gif':
                 case 'image/png':
diff --git a/lib/default.php b/lib/default.php
index d2270fa6a5..65301bbb45 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -303,6 +303,7 @@ $default =
                             'ClientSideShorten' => array(),
                             'EmailAuthentication' => array(),
                             'Event' => array(),
+                            'Oembed' => array(),
                             'OpenID' => array(),
                             'Poll' => array(),
                             'QnA' => array(),
diff --git a/lib/router.php b/lib/router.php
index 71bf4e8e82..3febbbeff4 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -168,9 +168,6 @@ class Router
 
             $m->connect('main/tagprofile', array('action' => 'tagprofile'));
 
-            $m->connect('main/oembed',
-                        array('action' => 'oembed'));
-
             $m->connect('main/xrds',
                         array('action' => 'publicxrds'));
 
diff --git a/plugins/Bookmark/README b/plugins/Bookmark/README
new file mode 100644
index 0000000000..e853760544
--- /dev/null
+++ b/plugins/Bookmark/README
@@ -0,0 +1 @@
+Depends on the oEmbed plugin (Oembed).
diff --git a/plugins/LinkPreview/README b/plugins/LinkPreview/README
new file mode 100644
index 0000000000..a1b1a87303
--- /dev/null
+++ b/plugins/LinkPreview/README
@@ -0,0 +1 @@
+Depends on the oEmbed plugin (Oembed)
diff --git a/plugins/Oembed/CONFIGURE b/plugins/Oembed/CONFIGURE
new file mode 100644
index 0000000000..b8d24f909f
--- /dev/null
+++ b/plugins/Oembed/CONFIGURE
@@ -0,0 +1,18 @@
+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.
diff --git a/plugins/Oembed/OembedPlugin.php b/plugins/Oembed/OembedPlugin.php
new file mode 100644
index 0000000000..177fd6ed3e
--- /dev/null
+++ b/plugins/Oembed/OembedPlugin.php
@@ -0,0 +1,187 @@
+<?php
+
+class OembedPlugin extends Plugin
+{
+    public function onCheckSchema()
+    {
+        $schema = Schema::get();
+        $schema->ensureTable('file_oembed', File_oembed::schemaDef());
+        return true;
+    }
+
+    public function onRouterInitialized(URLMapper $m)
+    {
+        $m->connect('main/oembed', array('action' => 'oembed'));
+    }
+
+    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->id)))),
+                'title'=>'oEmbed'),null);
+            $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->id)))),
+                'title'=>'oEmbed'),null);
+            break;
+        case 'shownotice':
+            $this->element('link',array('rel'=>'alternate',
+                'type'=>'application/json+oembed',
+                'href'=>common_local_url(
+                    'oembed',
+                    array(),
+                    array('format'=>'json','url'=>$this->notice->getUrl())),
+                'title'=>'oEmbed'),null);
+            $this->element('link',array('rel'=>'alternate',
+                'type'=>'text/xml+oembed',
+                'href'=>common_local_url(
+                    'oembed',
+                    array(),
+                    array('format'=>'xml','url'=>$this->notice->getUrl())),
+                'title'=>'oEmbed'),null);
+            break;
+        }
+
+        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.
+     * @param array  $redir_data lookup data eg from File_redirection::where()
+     * @param string $given_url
+     *
+     * @return boolean success
+     */
+    public function onEndFileSaveNew(File $file, array $redir_data, $given_url)
+    {
+        $fo = File_oembed::getKV('file_id', $file->id);
+        if ($fo instanceof File_oembed) {
+            common_log(LOG_WARNING, "Strangely, a File_oembed object exists for new file $file_id", __FILE__);
+             return true;
+        }
+
+        if (isset($redir_data['oembed']['json'])
+                && !empty($redir_data['oembed']['json'])) {
+            File_oembed::saveNew($redir_data['oembed']['json'], $file->id);
+        } elseif (isset($redir_data['type'])
+                && (('text/html' === substr($redir_data['type'], 0, 9)
+                || 'application/xhtml+xml' === substr($redir_data['type'], 0, 21)))) {
+
+            try {
+                $oembed_data = File_oembed::_getOembed($given_url);
+                if ($oembed_data === false) {
+                    throw new Exception('Did not get oEmbed data from URL');
+                }
+            } catch (Exception $e) {
+                return true;
+            }
+
+            File_oembed::saveNew($oembed_data, $file->id);
+        }
+        return true;
+    }
+
+    public function onEndShowAttachmentLink(HTMLOutputter $out, File $file)
+    {
+        $oembed = File_oembed::getKV('file_id', $file->id);
+        if (empty($oembed->author_name) && empty($oembed->provider)) {
+            return true;
+        }
+        $out->elementStart('div', array('id'=>'oembed_info', 'class'=>'entry-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->id);
+        if (!$oembed instanceof File_oembed || !in_array($oembed->type, array('photo', 'video'))) {
+            return true;
+        }
+
+        foreach (array('mimetype', 'url', 'title', 'modified') as $key) {
+            if (!empty($oembed->{$key})) {
+                $enclosure->{$key} = $oembed->{$key};
+            }
+        }
+        return true;
+    }
+    
+    public function onStartShowAttachmentRepresentation(HTMLOutputter $out, File $file)
+    {
+        $oembed = File_oembed::getKV('file_id', $file->id);
+        if (empty($oembed->type)) {
+            return true;
+        }
+        switch ($oembed->type) {
+        case 'rich':
+        case 'video':
+        case 'link':
+            if (!empty($oembed->html)) {
+                require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
+                $config = array(
+                    'safe'=>1,
+                    'elements'=>'*+object+embed');
+                $out->raw(htmLawed($oembed->html,$config));
+            }
+            break;
+
+        case 'photo':
+            $out->element('img', array('src' => $oembed->url, 'width' => $oembed->width, 'height' => $oembed->height, 'alt' => 'alt'));
+            break;
+
+        default:
+            Event::handle('ShowUnsupportedAttachmentRepresentation', array($out, $file));
+        }
+    }
+
+    public function onPluginVersion(array &$versions)
+    {
+        $versions[] = array('name' => 'Oembed',
+                            'version' => GNUSOCIAL_VERSION,
+                            'author' => 'Mikael Nordfeldth',
+                            'homepage' => 'http://gnu.io/',
+                            'description' =>
+                            // TRANS: Plugin description.
+                            _m('Plugin for using and representing Oembed data.'));
+        return true;
+    }
+}
diff --git a/plugins/Oembed/README b/plugins/Oembed/README
new file mode 100644
index 0000000000..be8c09f7a6
--- /dev/null
+++ b/plugins/Oembed/README
@@ -0,0 +1 @@
+It's really called oEmbed.
diff --git a/plugins/Oembed/actions/oembed.php b/plugins/Oembed/actions/oembed.php
new file mode 100644
index 0000000000..509c3a73ee
--- /dev/null
+++ b/plugins/Oembed/actions/oembed.php
@@ -0,0 +1,236 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * oEmbed data action for /main/oembed(.xml|.json) requests
+ *
+ * PHP version 5
+ *
+ * LICENCE: 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/>.
+ */
+
+if (!defined('GNUSOCIAL')) { exit(1); }
+
+/**
+ * Oembed provider implementation
+ *
+ * This class handles all /main/oembed(.xml|.json)/ requests.
+ *
+ * @category  oEmbed
+ * @package   GNUsocial
+ * @author    Craig Andrews <candrews@integralblue.com>
+ * @author    Mikael Nordfeldth <mmn@hethane.se>
+ * @copyright 2008 StatusNet, Inc.
+ * @copyright 2014 Free Software Foundation, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://status.net/
+ */
+
+class OembedAction extends Action
+{
+    protected function handle()
+    {
+        parent::handle();
+
+        $url = $this->trimmed('url');
+        if (substr(strtolower($url),0,strlen(common_root_url())) == strtolower(common_root_url())) {
+            $path = substr($url,strlen(common_root_url()));
+
+            $r = Router::get();
+
+            $proxy_args = $r->map($path);
+            if (!$proxy_args) {
+                // TRANS: Server error displayed in oEmbed action when path not found.
+                // TRANS: %s is a path.
+                $this->serverError(sprintf(_('"%s" not found.'),$path), 404);
+            }
+
+            $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';
+                $id = $proxy_args['notice'];
+                $notice = Notice::getKV($id);
+                if(empty($notice)){
+                    // TRANS: Server error displayed in oEmbed action when notice not found.
+                    // TRANS: %s is a notice.
+                    $this->serverError(sprintf(_("Notice %s not found."),$id), 404);
+                }
+                $profile = $notice->getProfile();
+                if (empty($profile)) {
+                    // TRANS: Server error displayed in oEmbed action when notice has not profile.
+                    $this->serverError(_('Notice has no profile.'), 500);
+                }
+                $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->rendered;
+                break;
+
+            case 'attachment':
+                $id = $proxy_args['attachment'];
+                $attachment = File::getKV($id);
+                if(empty($attachment)){
+                    // TRANS: Server error displayed in oEmbed action when attachment not found.
+                    // TRANS: %d is an attachment ID.
+                    $this->serverError(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);
+            }
+        } else {
+            // TRANS: Error message displaying attachments. %s is the site's base URL.
+            $this->serverError(sprintf(_('Only %s URLs over plain HTTP please.'), common_root_url()), 404);
+        }
+    }
+
+    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?
+     */
+    function isReadOnly($args)
+    {
+        return true;
+    }
+}
diff --git a/classes/File_oembed.php b/plugins/Oembed/classes/File_oembed.php
similarity index 100%
rename from classes/File_oembed.php
rename to plugins/Oembed/classes/File_oembed.php
diff --git a/lib/oembedhelper.php b/plugins/Oembed/lib/oembedhelper.php
similarity index 100%
rename from lib/oembedhelper.php
rename to plugins/Oembed/lib/oembedhelper.php
diff --git a/scripts/fixup_files.php b/plugins/Oembed/scripts/fixup_files.php
similarity index 89%
rename from scripts/fixup_files.php
rename to plugins/Oembed/scripts/fixup_files.php
index 18feaf2218..50ddc8a994 100755
--- a/scripts/fixup_files.php
+++ b/plugins/Oembed/scripts/fixup_files.php
@@ -18,7 +18,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
 
 $longoptions = array('dry-run');
 
@@ -62,11 +62,8 @@ while ($f->fetch()) {
         $f->decache();
         
         if (is_array($data)) {
-            if ($f->saveOembed($data, $f->url)) {
-                echo " (ok)\n";
-            } else {
-                echo " (ok, no embedding data)\n";
-            }
+            Event::handle('EndFileSaveNew', array($f, $data, $f->url));
+            echo " (ok)\n";
         } else {
             echo " (ok, but embedding lookup failed)\n";
         }
diff --git a/tests/oEmbedTest.php b/plugins/Oembed/tests/oEmbedTest.php
similarity index 98%
rename from tests/oEmbedTest.php
rename to plugins/Oembed/tests/oEmbedTest.php
index 50f3fd2919..49e070cb6d 100644
--- a/tests/oEmbedTest.php
+++ b/plugins/Oembed/tests/oEmbedTest.php
@@ -5,7 +5,7 @@ if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
     exit();
 }
 
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
 define('GNUSOCIAL', true);
 define('STATUSNET', true);  // compatibility
 
-- 
2.39.5