From f2ddd4471248aca3e73e16683aef0a10caff661b Mon Sep 17 00:00:00 2001
From: Kane York <rikingcoding@gmail.com>
Date: Sat, 13 Feb 2016 10:29:53 -0800
Subject: [PATCH] FEATURE: Add /search discovery

The opensearch.xml results in a "site search engine" being added to
Chrome, while the sitelinks search tag results in "Search this website"
being added to Google Search.
---
 ...n_controller.rb => metadata_controller.rb} |  8 ++++--
 app/controllers/site_controller.rb            |  5 ++--
 app/helpers/application_helper.rb             | 14 ++++++++++
 app/views/layouts/_head.html.erb              |  2 ++
 app/views/metadata/opensearch.xml.erb         | 12 ++++++++
 config/routes.rb                              |  3 +-
 .../manifest_json_controller_spec.rb          | 12 --------
 spec/controllers/metadata_controller_spec.rb  | 28 +++++++++++++++++++
 spec/rails_helper.rb                          |  7 +++++
 9 files changed, 73 insertions(+), 18 deletions(-)
 rename app/controllers/{manifest_json_controller.rb => metadata_controller.rb} (74%)
 create mode 100644 app/views/metadata/opensearch.xml.erb
 delete mode 100644 spec/controllers/manifest_json_controller_spec.rb
 create mode 100644 spec/controllers/metadata_controller_spec.rb

diff --git a/app/controllers/manifest_json_controller.rb b/app/controllers/metadata_controller.rb
similarity index 74%
rename from app/controllers/manifest_json_controller.rb
rename to app/controllers/metadata_controller.rb
index 8c7b6bc42..f5b8beccc 100644
--- a/app/controllers/manifest_json_controller.rb
+++ b/app/controllers/metadata_controller.rb
@@ -1,8 +1,8 @@
-class ManifestJsonController < ApplicationController
+class MetadataController < ApplicationController
   layout false
   skip_before_filter :preload_json, :check_xhr, :redirect_to_login_if_required
 
-  def index
+  def manifest
     manifest = {
       short_name: SiteSetting.title,
       display: 'standalone',
@@ -14,4 +14,8 @@ class ManifestJsonController < ApplicationController
 
     render json: manifest.to_json
   end
+
+  def opensearch
+    render file: "#{Rails.root}/app/views/metadata/opensearch.xml"
+  end
 end
diff --git a/app/controllers/site_controller.rb b/app/controllers/site_controller.rb
index 6597e7e76..f73a568f0 100644
--- a/app/controllers/site_controller.rb
+++ b/app/controllers/site_controller.rb
@@ -1,8 +1,8 @@
 require_dependency 'site_serializer'
 
 class SiteController < ApplicationController
-
-  skip_before_filter :preload_json
+  layout false
+  skip_before_filter :preload_json, :check_xhr
 
   def site
     render json: Site.json_for(guardian)
@@ -23,5 +23,4 @@ class SiteController < ApplicationController
   def emoji
     render json: custom_emoji
   end
-
 end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index c007b461d..0b609688b 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -157,6 +157,20 @@ module ApplicationHelper
     result.join("\n")
   end
 
+  def render_sitelinks_search_tag
+    json = {
+      '@context': 'http://schema.org',
+      '@type': 'WebSite',
+      url: Discourse.base_url,
+      potentialAction: {
+        '@type': 'SearchAction',
+        target: "#{Discourse.base_url}/search?q={search_term_string}",
+        'query-input': 'required name=search_term_string',
+      }
+    }
+    content_tag(:script, MultiJson.dump(json).html_safe, type: 'application/ld+json'.freeze)
+  end
+
   def application_logo_url
     @application_logo_url ||= (mobile_view? && SiteSetting.mobile_logo_url) || SiteSetting.logo_url
   end
diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb
index 0843f7133..6a5ab0ee6 100644
--- a/app/views/layouts/_head.html.erb
+++ b/app/views/layouts/_head.html.erb
@@ -12,3 +12,5 @@
 <meta name="theme-color" content="#<%= ColorScheme.hex_for_name('header_background') %>">
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes">
 <%= canonical_link_tag %>
+<%= render_sitelinks_search_tag %>
+<link rel="search" type="application/opensearchdescription+xml" href="<%= Discourse.base_url %>/opensearch.xml">
diff --git a/app/views/metadata/opensearch.xml.erb b/app/views/metadata/opensearch.xml.erb
new file mode 100644
index 000000000..ff47bada5
--- /dev/null
+++ b/app/views/metadata/opensearch.xml.erb
@@ -0,0 +1,12 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+<ShortName><%= SiteSetting.title %> Search</ShortName>
+<Description>Search for posts on <%= SiteSetting.title %></Description>
+<Tags>discourse forum</Tags>
+<% if SiteSetting.favicon_url =~ /\.ico$/ -%>
+  <Image height="16" width="16" type="image/vnd.microsoft.icon"><%= UrlHelper.absolute SiteSetting.favicon_url %></Image>
+<%- else -%>
+  <Image type="image/png"><%= UrlHelper.absolute SiteSetting.favicon_url %></Image>
+<%- end %>
+<Url type="text/html" method="get" template="<%= Discourse.base_url %>/search?q={searchTerms}"/>
+<Query role="example" searchTerms="search term"/>
+</OpenSearchDescription>
diff --git a/config/routes.rb b/config/routes.rb
index c8de5b650..7d70c38db 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -599,7 +599,8 @@ Discourse::Application.routes.draw do
   get "favicon/proxied" => "static#favicon", format: false
 
   get "robots.txt" => "robots_txt#index"
-  get "manifest.json" => "manifest_json#index", as: :manifest
+  get "manifest.json" => "metadata#manifest", as: :manifest
+  get "opensearch" => "metadata#opensearch", format: :xml
 
   Discourse.filters.each do |filter|
     root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), :as => "list_#{filter}"
diff --git a/spec/controllers/manifest_json_controller_spec.rb b/spec/controllers/manifest_json_controller_spec.rb
deleted file mode 100644
index d0fd39e83..000000000
--- a/spec/controllers/manifest_json_controller_spec.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe ManifestJsonController do
-  context 'index' do
-    it 'returns the right output' do
-      title = 'MyApp'
-      SiteSetting.title = title
-      get :index
-      expect(response.body).to include(title)
-    end
-  end
-end
diff --git a/spec/controllers/metadata_controller_spec.rb b/spec/controllers/metadata_controller_spec.rb
new file mode 100644
index 000000000..681cbe072
--- /dev/null
+++ b/spec/controllers/metadata_controller_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+RSpec.describe MetadataController do
+  describe 'manifest.json' do
+    it 'returns the right output' do
+      title = 'MyApp'
+      SiteSetting.title = title
+      get :manifest
+      expect(response.body).to include(title)
+      expect(response.content_type).to eq('application/json')
+    end
+  end
+
+  describe 'opensearch.xml' do
+    it 'returns the right output' do
+      title = 'MyApp'
+      favicon_path = '/uploads/something/23432.png'
+      SiteSetting.title = title
+      SiteSetting.favicon_url = favicon_path
+      get :opensearch, format: :xml
+      expect(response.body).to include(title)
+      expect(response.body).to include("/search?q={searchTerms}")
+      expect(response.body).to include('image/png')
+      expect(response.body).to include(favicon_path)
+      expect(response.content_type).to eq('application/xml')
+    end
+  end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 30921a6e3..354cb630d 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -78,6 +78,13 @@ Spork.prefork do
         SiteSetting.defaults[k] = v
       end
 
+      # Monkey patch for NoMethodError: undefined method `cache' for nil:NilClass
+      # https://github.com/rspec/rspec-rails/issues/1532#issuecomment-174679485
+      # fixed in Rspec 3.4.1
+      RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator.class_eval do
+        alias_method :find_all_anywhere, :find_all
+      end
+
       require_dependency 'site_settings/local_process_provider'
       SiteSetting.provider = SiteSettings::LocalProcessProvider.new
     end