From f3ea8f424fb79d3f0143ccde6e170965b23926be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20Sch=C3=B6lling?= <manuel.schoelling@gmx.de>
Date: Sun, 25 Sep 2011 21:59:25 +0200
Subject: [PATCH] Added oEmbed support

---
 Gemfile                                       |   1 +
 app/helpers/markdownify_helper.rb             |   6 +-
 app/models/jobs/gather_o_embed_data.rb        |  38 ++++++
 app/models/o_embed_cache.rb                   |   3 +
 app/models/post.rb                            |   5 +
 app/views/comments/_comment.html.haml         |   2 +-
 app/views/messages/_message.haml              |   2 +-
 app/views/people/_profile_sidebar.html.haml   |   4 +-
 app/views/photos/show.html.haml               |   2 +-
 .../status_messages/_status_message.haml      |   2 +-
 config/initializers/oembed.rb                 |   4 +
 .../20110924112840_create_o_embed_caches.rb   |  14 ++
 lib/diaspora/markdownify.rb                   |   3 +-
 spec/helpers/markdownify_helper_spec.rb       | 128 ++++++++++++++++++
 spec/models/jobs/gather_o_embed_data.rb       |  59 ++++++++
 spec/models/o_embed_cache_spec.rb             |   5 +
 spec/models/post_spec.rb                      |  11 ++
 17 files changed, 280 insertions(+), 9 deletions(-)
 create mode 100644 app/models/jobs/gather_o_embed_data.rb
 create mode 100644 app/models/o_embed_cache.rb
 create mode 100644 config/initializers/oembed.rb
 create mode 100644 db/migrate/20110924112840_create_o_embed_caches.rb
 create mode 100644 spec/models/jobs/gather_o_embed_data.rb
 create mode 100644 spec/models/o_embed_cache_spec.rb

diff --git a/Gemfile b/Gemfile
index a32c2ca195..66326040a1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -64,6 +64,7 @@ gem 'rails-i18n'
 gem 'nokogiri'
 gem 'redcarpet', "2.0.0b5"
 gem 'roxml', :git => 'git://github.com/Empact/roxml.git', :ref => '7ea9a9ffd2338aaef5b0'
+gem 'ruby-oembed'
 
 # queue
 
diff --git a/app/helpers/markdownify_helper.rb b/app/helpers/markdownify_helper.rb
index cf012b581e..4b87e62105 100644
--- a/app/helpers/markdownify_helper.rb
+++ b/app/helpers/markdownify_helper.rb
@@ -33,7 +33,11 @@ module MarkdownifyHelper
     return '' if message.blank?
 
     #renderer = Redcarpet::Render::HTML.new(render_options)
-    renderer = Diaspora::Markdownify::HTML.new(render_options)
+    if render_options[:oembed]
+      renderer = Diaspora::Markdownify::HTMLwithOEmbed.new(render_options)
+    else
+      renderer = Diaspora::Markdownify::HTML.new(render_options)
+    end
     markdown = Redcarpet::Markdown.new(renderer, markdown_options)
 
     message = markdown.render(message).html_safe
diff --git a/app/models/jobs/gather_o_embed_data.rb b/app/models/jobs/gather_o_embed_data.rb
new file mode 100644
index 0000000000..ed70e176e6
--- /dev/null
+++ b/app/models/jobs/gather_o_embed_data.rb
@@ -0,0 +1,38 @@
+#   Copyright (c) 2010-2011, Diaspora Inc.  This file is
+#   licensed under the Affero General Public License version 3 or later.  See
+#   the COPYRIGHT file.
+#
+
+module Jobs
+  class GatherOEmbedData < Base
+    @queue = :http_service
+
+    class GatherOEmbedRenderer < Redcarpet::Render::HTML
+      include ActionView::Helpers::TextHelper
+      include ActionView::Helpers::TagHelper
+
+      def autolink(link, type)
+        return link if OEmbedCache.exists?(:url => link)
+        
+        begin
+          res = ::OEmbed::Providers.get(link, {:maxwidth => 420, :maxheight => 420, :frame => 1, :iframe => 1})
+        rescue Exception => e
+          # noop
+        else
+          data = res.fields
+          data['trusted_endpoint_url'] = res.provider.endpoint
+          cache = OEmbedCache.new(:url => link, :data => data)
+          cache.save
+        end
+        
+        return link
+      end
+    end
+
+    def self.perform(text)
+      renderer = GatherOEmbedRenderer.new({})
+      markdown = Redcarpet::Markdown.new(renderer, {:autolink => true})
+      message = markdown.render(text)
+    end
+  end
+end
diff --git a/app/models/o_embed_cache.rb b/app/models/o_embed_cache.rb
new file mode 100644
index 0000000000..79f472f8c8
--- /dev/null
+++ b/app/models/o_embed_cache.rb
@@ -0,0 +1,3 @@
+class OEmbedCache < ActiveRecord::Base
+  serialize :data
+end
diff --git a/app/models/post.rb b/app/models/post.rb
index 178167438e..20613b5a5e 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -66,6 +66,11 @@ class Post < ActiveRecord::Base
     @reshare_count ||= Post.where(:root_guid => self.guid).count
   end
 
+  def text= nd
+    Resque.enqueue(Jobs::GatherOEmbedData, nd)
+    write_attribute(:text, nd)
+  end
+
   def diaspora_handle= nd
     self.author = Person.where(:diaspora_handle => nd).first
     write_attribute(:diaspora_handle, nd)
diff --git a/app/views/comments/_comment.html.haml b/app/views/comments/_comment.html.haml
index fd8a9834ef..e0955bde36 100644
--- a/app/views/comments/_comment.html.haml
+++ b/app/views/comments/_comment.html.haml
@@ -12,7 +12,7 @@
       = person_link(comment.author, :class => "hovercardable")
 
     %span{:class => direction_for(comment.text)}
-      = markdownify(comment, :youtube_maps => comment.youtube_titles)
+      = markdownify(comment, :oembed => true, :youtube_maps => comment.youtube_titles)
 
     .comment_info
       %time.timeago{:datetime => comment.created_at}
diff --git a/app/views/messages/_message.haml b/app/views/messages/_message.haml
index f36ebbc55c..ad44c9b796 100644
--- a/app/views/messages/_message.haml
+++ b/app/views/messages/_message.haml
@@ -12,5 +12,5 @@
         = how_long_ago(message)
 
     %div{ :class => direction_for(message.text) }
-      = markdownify(message)
+      = markdownify(message, :oembed => true)
 
diff --git a/app/views/people/_profile_sidebar.html.haml b/app/views/people/_profile_sidebar.html.haml
index c6d85c2057..b3c5a77392 100644
--- a/app/views/people/_profile_sidebar.html.haml
+++ b/app/views/people/_profile_sidebar.html.haml
@@ -22,13 +22,13 @@
             %h4
               =t('.bio')
             %div{ :class => direction_for(person.profile.bio) }
-              = markdownify(person.profile.bio, :newlines => true)
+              = markdownify(person.profile.bio, :oembed => true, :newlines => true)
         - unless person.profile.location.blank?
           %li
             %h4
               =t('.location')
             %div{ :class => direction_for(person.profile.location) }
-              = markdownify(person.profile.location, :newlines => true)
+              = markdownify(person.profile.location, :oembed => true, :newlines => true)
 
         %li
           - unless person.profile.gender.blank?
diff --git a/app/views/photos/show.html.haml b/app/views/photos/show.html.haml
index 283580bb0c..f9805f2322 100644
--- a/app/views/photos/show.html.haml
+++ b/app/views/photos/show.html.haml
@@ -40,7 +40,7 @@
 
     .span-8.last
       %p
-        = markdownify(photo.status_message)
+        = markdownify(photo.status_message, :oembed => true)
       %span{:style=>'font-size:smaller'}
         =link_to t('.collection_permalink'), post_path(photo.status_message)
       %br
diff --git a/app/views/status_messages/_status_message.haml b/app/views/status_messages/_status_message.haml
index ec8343327f..2a6347d668 100644
--- a/app/views/status_messages/_status_message.haml
+++ b/app/views/status_messages/_status_message.haml
@@ -16,4 +16,4 @@
           = link_to (image_tag photo.url(:thumb_small), :class => 'stream-photo thumb_small', 'data-small-photo' => photo.url(:thumb_medium), 'data-full-photo' => photo.url), photo_path(photo), :class => 'stream-photo-link'
 
 %div{:class => direction_for(post.text)}
-  != markdownify(post, :youtube_maps => post[:youtube_titles])
+  != markdownify(post, :oembed => true, :youtube_maps => post[:youtube_titles])
diff --git a/config/initializers/oembed.rb b/config/initializers/oembed.rb
new file mode 100644
index 0000000000..5db1954342
--- /dev/null
+++ b/config/initializers/oembed.rb
@@ -0,0 +1,4 @@
+require 'oembed'
+OEmbed::Providers.register_all
+OEmbed::Providers.register_fallback(OEmbed::ProviderDiscovery)
+
diff --git a/db/migrate/20110924112840_create_o_embed_caches.rb b/db/migrate/20110924112840_create_o_embed_caches.rb
new file mode 100644
index 0000000000..78a549e818
--- /dev/null
+++ b/db/migrate/20110924112840_create_o_embed_caches.rb
@@ -0,0 +1,14 @@
+class CreateOEmbedCaches < ActiveRecord::Migration
+  def self.up
+    create_table :o_embed_caches do |t|
+      t.string :url, :limit => 1024, :null => false, :unique => true
+      t.text :data, :null => false
+    end
+    add_index :o_embed_caches, :url
+  end
+
+  def self.down
+    remove_index :o_embed_caches, :column => :url
+    drop_table :o_embed_caches
+  end
+end
diff --git a/lib/diaspora/markdownify.rb b/lib/diaspora/markdownify.rb
index 900436eb88..bdd24da541 100644
--- a/lib/diaspora/markdownify.rb
+++ b/lib/diaspora/markdownify.rb
@@ -3,12 +3,11 @@ require 'erb'
 module Diaspora
   module Markdownify
     class HTML < Redcarpet::Render::HTML
-
       include ActionView::Helpers::TextHelper
       include ActionView::Helpers::TagHelper
 
       def autolink(link, type)
-        auto_link(link, :link => :urls, :html => { :target => "_blank" })
+        auto_link(link, :link => :urls)
       end
     end
   end
diff --git a/spec/helpers/markdownify_helper_spec.rb b/spec/helpers/markdownify_helper_spec.rb
index ed73c7d1b0..af88b27ba6 100644
--- a/spec/helpers/markdownify_helper_spec.rb
+++ b/spec/helpers/markdownify_helper_spec.rb
@@ -58,6 +58,134 @@ describe MarkdownifyHelper do
         formatted = markdownify(message)
         formatted.should =~ /hovercard/
       end
+
+      context 'when posting a link with oEmbed support' do
+        scenarios = {
+          "photo" => {
+            "oembed_data" => {
+              "trusted_endpoint_url" => "__!SPOOFED!__",
+              "version" => "1.0",
+              "type" => "photo",
+              "title" => "ZB8T0193",
+              "width" => "240",
+              "height" => "160",
+              "url" => "http://farm4.static.flickr.com/3123/2341623661_7c99f48bbf_m.jpg"
+            },
+            "link_url" => 'http://www.flickr.com/photos/bees/2341623661',
+            "oembed_get_request" => "http://www.flickr.com/services/oembed/?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://www.flickr.com/photos/bees/2341623661",
+          },
+
+          "unsupported" => {
+            "oembed_data" => "",
+            "oembed_get_request" => 'http://www.we-do-not-support-oembed.com/index.html',
+            "link_url" => 'http://www.we-do-not-support-oembed.com/index.html'
+          },
+
+          "secure_video" => {
+            "oembed_data" => {
+	            "version" => "1.0",
+	            "type" => "video",
+	            "width" => 425,
+	            "height" => 344,
+	            "title" => "Amazing Nintendo Facts",
+	            "html" => "<object width=\"425\" height=\"344\">
+			            <param name=\"movie\" value=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"></param>
+			            <param name=\"allowFullScreen\" value=\"true\"></param>
+			            <param name=\"allowscriptaccess\" value=\"always\"></param>
+			            <embed src=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"
+				            type=\"application/x-shockwave-flash\" width=\"425\" height=\"344\"
+				            allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed>
+		            </object>",
+            },
+            "link_url" => "http://youtube.com/watch?v=M3r2XDceM6A&format=json",
+            "oembed_get_request" => "http://www.youtube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://youtube.com/watch?v=M3r2XDceM6A",
+          },
+
+          "unsecure_video" => {
+            "oembed_data" => {
+	            "version" => "1.0",
+	            "type" => "video",
+	            "title" => "This is a video from an unsecure source",
+	            "html" => "<object width=\"425\" height=\"344\">
+			            <param name=\"movie\" value=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"></param>
+			            <param name=\"allowFullScreen\" value=\"true\"></param>
+			            <param name=\"allowscriptaccess\" value=\"always\"></param>
+			            <embed src=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"
+				            type=\"application/x-shockwave-flash\" width=\"425\" height=\"344\"
+				            allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed>
+		            </object>",
+            },
+            "link_url" => "http://mytube.com/watch?v=M3r2XDceM6A&format=json",
+            "discovery_data" => '<link rel="alternate" type="application/json+oembed" href="http://www.mytube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://mytube.com/watch?v=M3r2XDceM6A" />',
+            "oembed_get_request" => "http://www.mytube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://mytube.com/watch?v=M3r2XDceM6A",
+          },
+
+          "secure_rich" => {
+            "oembed_data" => {
+	            "version" => "1.0",
+	            "type" => "rich",
+	            "width" => 425,
+	            "height" => 344,
+	            "title" => "Amazing Nintendo Facts",
+	            "html" => "<object width=\"425\" height=\"344\">
+			            <param name=\"movie\" value=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"></param>
+			            <param name=\"allowFullScreen\" value=\"true\"></param>
+			            <param name=\"allowscriptaccess\" value=\"always\"></param>
+			            <embed src=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"
+				            type=\"application/x-shockwave-flash\" width=\"425\" height=\"344\"
+				            allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed>
+		            </object>",
+            },
+            "link_url" => "http://youtube.com/watch?v=M3r2XDceM6A&format=json",
+            "oembed_get_request" => "http://www.youtube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://youtube.com/watch?v=M3r2XDceM6A",
+          },
+
+          "unsecure_rich" => {
+            "oembed_data" => {
+	            "version" => "1.0",
+	            "type" => "rich",
+	            "title" => "This is a video from an unsecure source",
+	            "html" => "<object width=\"425\" height=\"344\">
+			            <param name=\"movie\" value=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"></param>
+			            <param name=\"allowFullScreen\" value=\"true\"></param>
+			            <param name=\"allowscriptaccess\" value=\"always\"></param>
+			            <embed src=\"http://www.youtube.com/v/M3r2XDceM6A&fs=1\"
+				            type=\"application/x-shockwave-flash\" width=\"425\" height=\"344\"
+				            allowscriptaccess=\"always\" allowfullscreen=\"true\"></embed>
+		            </object>",
+            },
+            "link_url" => "http://mytube.com/watch?v=M3r2XDceM6A&format=json",
+            "discovery_data" => '<link rel="alternate" type="application/json+oembed" href="http://www.mytube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://mytube.com/watch?v=M3r2XDceM6A" />',
+            "oembed_get_request" => "http://www.mytube.com/oembed?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url=http://mytube.com/watch?v=M3r2XDceM6A",
+          },
+        }
+
+        scenarios.each do |type, data|
+          specify 'for type "'+type+'"' do
+            stub_request(:get, data['oembed_get_request']).to_return(:status => 200, :body => data['oembed_data'].to_json.to_s)
+            stub_request(:get, data['link_url']).to_return(:status => 200, :body => data['discovery_data']) if data.has_key?('discovery_data')
+
+            message = "Look at this! "+data['link_url']
+            Jobs::GatherOEmbedData.perform(message)
+            OEmbedCache.find_by_url(data['link_url']).should_not be_nil unless type == 'unsupported'
+
+            formatted = markdownify(message, :oembed => true)
+            case type
+              when 'photo'
+                formatted.should =~ /#{data['oembed_data']['url']}/
+              when 'unsupported'
+                formatted.should =~ /#{data['link_url']}/
+              when 'secure_video', 'secure_rich'
+                formatted.should =~ /#{data['oembed_data']['html']}/
+              when 'unsecure_video', 'unsecure_rich'
+                formatted.should_not =~ /#{data['oembed_data']['html']}/
+                formatted.should =~ /#{data['oembed_data']['title']}/
+                formatted.should =~ /#{data['oembed_data']['url']}/
+            end
+          end
+        end
+
+      end
     end
   end
 end
diff --git a/spec/models/jobs/gather_o_embed_data.rb b/spec/models/jobs/gather_o_embed_data.rb
new file mode 100644
index 0000000000..5d95a2ff85
--- /dev/null
+++ b/spec/models/jobs/gather_o_embed_data.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+describe Jobs::GatherOEmbedData do
+  before do
+    @flickr_oembed_data = {
+      "trusted_endpoint_url" => "__!SPOOFED!__",
+      "version" => "1.0",
+      "type" => "photo",
+      "author_url" => "http://www.flickr.com/photos/bees/",
+      "cache_age" => 3600,
+      "provider_name" => "Flickr",
+      "provider_url" => "http://www.flickr.com/",
+      "title" => "ZB8T0193",
+      "author_name" => "Bees",
+      "width" => "240",
+      "height" => "160",
+      "url" => "https://farm4.static.flickr.com/3123/2341623661_7c99f48bbf_m.jpg"
+    }
+
+    @flickr_oembed_url = 'http://www.flickr.com/services/oembed/'
+    @flickr_photo_url = 'http://www.flickr.com/photos/bees/2341623661'
+    @flickr_oembed_get_request = @flickr_oembed_url+"?format=json&frame=1&iframe=1&maxheight=420&maxwidth=420&url="+@flickr_photo_url
+
+    @no_oembed_url = 'http://www.we-do-not-support-oembed.com/index.html'
+
+    stub_request(:get, @flickr_oembed_get_request).to_return(:status => 200, :body => @flickr_oembed_data.to_json)
+    stub_request(:get, @no_oembed_url).to_return(:status => 200, :body => '<html><body>hello there</body></html>')
+  end
+
+  describe '.perform' do
+    it 'requests not data from the internet' do
+      Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url)
+
+      a_request(:get, @flickr_oembed_get_request).should have_been_made
+    end
+
+    it 'requests not data from the internet only once' do
+      Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url)
+      Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url)
+
+      a_request(:get, @flickr_oembed_get_request).should have_been_made.times(1)
+    end
+
+    it 'creates one cache entry' do
+      Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url)
+
+      expected_data = @flickr_oembed_data
+      expected_data['trusted_endpoint_url'] = @flickr_oembed_url
+      OEmbedCache.find_by_url(@flickr_photo_url).data.should == expected_data
+
+      Jobs::GatherOEmbedData.perform("Look at this! "+@flickr_photo_url)
+      OEmbedCache.count(:conditions => {:url => @flickr_photo_url}).should == 1
+    end
+
+    it 'creates no cache entry for unsupported pages' do
+      Jobs::GatherOEmbedData.perform("This page is lame! It does not support oEmbed: "+@no_oembed_url)
+      OEmbedCache.find_by_url(@no_oembed_url).should be_nil
+    end
+  end
+end
diff --git a/spec/models/o_embed_cache_spec.rb b/spec/models/o_embed_cache_spec.rb
new file mode 100644
index 0000000000..e544179624
--- /dev/null
+++ b/spec/models/o_embed_cache_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe OEmbedCache do
+  pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index fade3ab7ae..10a769bd8c 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -68,6 +68,17 @@ describe Post do
     end
   end
 
+  describe 'oEmbed' do
+    it 'should gather oEmbed data' do
+      stub_request(:get, "http://gdata.youtube.com/feeds/api/videos/M3r2XDceM6A?v=2").to_return(:status => 200, :body => "")
+
+      text = 'http://youtube.com/watch?v=M3r2XDceM6A&format=json'
+      Resque.should_receive(:enqueue).with(Jobs::GatherOEmbedData, text)
+      post = Factory.create(:status_message, :author => @user.person, :text => text)
+      post.save!
+    end
+  end
+
   describe 'serialization' do
     it 'should serialize the handle and not the sender' do
       post = @user.post :status_message, :text => "hello", :to => @aspect.id
-- 
GitLab