From 2e0b3826997221289ce64d826c7a921541d7732d Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Mon, 11 Jun 2018 18:21:06 +0300 Subject: [PATCH 1/4] Use 'post' prefix for status message route helpers This is required to make url_for(StatusMessage.new) work --- app/models/status_message.rb | 4 ++++ app/views/publisher/_publisher.html.haml | 2 +- app/views/publisher/_publisher.mobile.haml | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/status_message.rb b/app/models/status_message.rb index 17d267797..0c68286ae 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -37,6 +37,10 @@ class StatusMessage < Post owned_or_visible_by_user(person.owner).joins(:mentions).where(mentions: {person_id: person.id}) } + def self.model_name + Post.model_name + end + def self.guids_for_author(person) Post.connection.select_values(Post.where(:author_id => person.id).select('posts.guid').to_sql) end diff --git a/app/views/publisher/_publisher.html.haml b/app/views/publisher/_publisher.html.haml index 808a79060..3322d7619 100644 --- a/app/views/publisher/_publisher.html.haml +++ b/app/views/publisher/_publisher.html.haml @@ -1,6 +1,6 @@ .row.publisher#publisher{class: ((aspect == :profile || publisher_open?) ? "mention_popup" : "closed")} .content_creation - = form_for(StatusMessage.new) do |status| + = form_for StatusMessage.new, url: status_messages_path, as: :status_message do |status| = status.error_messages %params .publisher-textarea-wrapper#publisher-textarea-wrapper diff --git a/app/views/publisher/_publisher.mobile.haml b/app/views/publisher/_publisher.mobile.haml index 68681213b..e031c1027 100644 --- a/app/views/publisher/_publisher.mobile.haml +++ b/app/views/publisher/_publisher.mobile.haml @@ -2,7 +2,8 @@ -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. -= form_for StatusMessage.new, html: {class: "control-group", data: {ajax: false}} do |status| += form_for StatusMessage.new, url: status_messages_path, as: :status_message, + html: {class: "control-group", data: {ajax: false}} do |status| .form-group = status.hidden_field :provider_display_name, value: 'mobile' = status.text_area :text, placeholder: t('shared.publisher.whats_on_your_mind'), rows: 4, autofocus: "autofocus", class: "form-control" From 6f812a5b8f20c04ba1a7aa01dcec13d2a07ac1af Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Mon, 11 Jun 2018 04:23:18 +0300 Subject: [PATCH 2/4] Add LinksController LinksController redirects requests for provided diaspora:// links to respective entities urls. --- app/controllers/links_controller.rb | 16 +++++ app/models/reference.rb | 3 +- app/services/diaspora_link_service.rb | 45 +++++++++++++ config/routes.rb | 2 + lib/diaspora/entity_finder.rb | 22 +++++++ spec/controllers/links_controller_spec.rb | 71 +++++++++++++++++++++ spec/services/diaspora_link_service_spec.rb | 44 +++++++++++++ 7 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 app/controllers/links_controller.rb create mode 100644 app/services/diaspora_link_service.rb create mode 100644 lib/diaspora/entity_finder.rb create mode 100644 spec/controllers/links_controller_spec.rb create mode 100644 spec/services/diaspora_link_service_spec.rb diff --git a/app/controllers/links_controller.rb b/app/controllers/links_controller.rb new file mode 100644 index 000000000..f28fbbc64 --- /dev/null +++ b/app/controllers/links_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class LinksController < ApplicationController + def resolve + entity = DiasporaLinkService.new(query).find_or_fetch_entity + raise ActiveRecord::RecordNotFound if entity.nil? + + redirect_to url_for(entity) + end + + private + + def query + @query ||= params.fetch(:q) + end +end diff --git a/app/models/reference.rb b/app/models/reference.rb index 3de1ff20a..45501edd4 100644 --- a/app/models/reference.rb +++ b/app/models/reference.rb @@ -22,8 +22,7 @@ class Reference < ApplicationRecord private def add_reference(author, type, guid) - class_name = DiasporaFederation::Entity.entity_class(type).to_s.rpartition("::").last - entity = Diaspora::Federation::Mappings.model_class_for(class_name).find_by(guid: guid) + entity = Diaspora::EntityFinder.new(type, guid).find references.find_or_create_by(target: entity) if entity&.diaspora_handle == author rescue => e # rubocop:disable Lint/RescueWithoutErrorClass logger.warn "ignoring invalid diaspora-url: diaspora://#{author}/#{type}/#{guid}: #{e.class}: #{e.message}" diff --git a/app/services/diaspora_link_service.rb b/app/services/diaspora_link_service.rb new file mode 100644 index 000000000..1923aa01d --- /dev/null +++ b/app/services/diaspora_link_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Encapsulates logic of processing diaspora:// links +class DiasporaLinkService + attr_reader :type, :author, :guid + + def initialize(link) + @link = link.dup + parse + end + + def find_or_fetch_entity + entity_finder.find || fetch_entity + end + + private + + attr_accessor :link + + def fetch_entity + DiasporaFederation::Federation::Fetcher.fetch_public(author, type, guid) + entity_finder.find + rescue DiasporaFederation::Federation::Fetcher::NotFetchable + nil + end + + def entity_finder + @entity_finder ||= Diaspora::EntityFinder.new(type, guid) + end + + def normalize + link.gsub!(%r{^web\+diaspora://}, "diaspora://") || + link.gsub!(%r{^//}, "diaspora://") || + %r{^diaspora://}.match(link) || + self.link = "diaspora://#{link}" + end + + def parse + normalize + match = DiasporaFederation::Federation::DiasporaUrlParser::DIASPORA_URL_REGEX.match(link) + @author = match[1] + @type = match[2] + @guid = match[3] + end +end diff --git a/config/routes.rb b/config/routes.rb index 846921b8b..f7ad53bec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,8 @@ Rails.application.routes.draw do #Search get 'search' => "search#search" + get "link" => "links#resolve" + resources :conversations, except: %i(edit update destroy) do resources :messages, only: %i(create) delete 'visibility' => 'conversation_visibilities#destroy' diff --git a/lib/diaspora/entity_finder.rb b/lib/diaspora/entity_finder.rb new file mode 100644 index 000000000..0bf824382 --- /dev/null +++ b/lib/diaspora/entity_finder.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Diaspora + class EntityFinder + def initialize(type, guid) + @type = type + @guid = guid + end + + def class_name + @class_name ||= DiasporaFederation::Entity.entity_class(type).to_s.rpartition("::").last + end + + def find + Diaspora::Federation::Mappings.model_class_for(class_name).find_by(guid: guid) + end + + private + + attr_reader :type, :guid + end +end diff --git a/spec/controllers/links_controller_spec.rb b/spec/controllers/links_controller_spec.rb new file mode 100644 index 000000000..841b77cd4 --- /dev/null +++ b/spec/controllers/links_controller_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +describe LinksController, type: :controller do + describe "#resolve" do + context "with post" do + let(:post) { FactoryGirl.create(:status_message) } + let(:link_text) { "#{post.author.diaspora_handle}/post/#{post.guid}" } + subject { get :resolve, params: {q: link_query} } + + shared_examples "redirects to the post" do + it "redirects to the post" do + expect(subject).to redirect_to(post_url(post)) + end + end + + context "with stripped link text" do + let(:link_query) { link_text } + include_examples "redirects to the post" + end + + context "with link text starting with //" do + let(:link_query) { "//#{link_text}" } + include_examples "redirects to the post" + end + + context "with link text starting with diaspora://" do + let(:link_query) { "diaspora://#{link_text}" } + include_examples "redirects to the post" + end + + context "with link text starting with web+diaspora://" do + let(:link_query) { "web+diaspora://#{link_text}" } + include_examples "redirects to the post" + end + + context "when post is non-fetchable" do + let(:diaspora_id) { FactoryGirl.create(:person).diaspora_handle } + let(:guid) { "1234567890abcdef" } + let(:link_query) { "web+diaspora://#{diaspora_id}/post/#{guid}" } + + before do + expect(DiasporaFederation::Federation::Fetcher) + .to receive(:fetch_public) + .with(diaspora_id, "post", guid) + .and_raise(DiasporaFederation::Federation::Fetcher::NotFetchable) + end + + it "responds 404" do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context "when user is non-fetchable" do + let(:diaspora_id) { "unknown@pod.tld" } + let(:guid) { "1234567890abcdef" } + let(:link_query) { "web+diaspora://#{diaspora_id}/post/#{guid}" } + + before do + expect(Person) + .to receive(:find_or_fetch_by_identifier) + .with(diaspora_id) + .and_return(nil) + end + + it "responds 404" do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end +end diff --git a/spec/services/diaspora_link_service_spec.rb b/spec/services/diaspora_link_service_spec.rb new file mode 100644 index 000000000..bde411a27 --- /dev/null +++ b/spec/services/diaspora_link_service_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +describe DiasporaLinkService do + let(:service) { described_class.new(link) } + + describe "#find_or_fetch_entity" do + context "when entity is known" do + let(:post) { FactoryGirl.create(:status_message) } + let(:link) { "diaspora://#{post.author.diaspora_handle}/post/#{post.guid}" } + + it "returns the entity" do + expect(service.find_or_fetch_entity).to eq(post) + end + end + + context "when entity is unknown" do + let(:remote_person) { FactoryGirl.create(:person) } + let(:guid) { "1234567890abcdef" } + let(:link) { "diaspora://#{remote_person.diaspora_handle}/post/#{guid}" } + + it "fetches entity" do + expect(DiasporaFederation::Federation::Fetcher) + .to receive(:fetch_public) + .with(remote_person.diaspora_handle, "post", guid) { + FactoryGirl.create(:status_message, author: remote_person, guid: guid) + } + + entity = service.find_or_fetch_entity + expect(entity).to be_a(StatusMessage) + expect(entity.guid).to eq(guid) + expect(entity.author).to eq(remote_person) + end + + it "returns nil when entity is non fetchable" do + expect(DiasporaFederation::Federation::Fetcher) + .to receive(:fetch_public) + .with(remote_person.diaspora_handle, "post", guid) + .and_raise(DiasporaFederation::Federation::Fetcher::NotFetchable) + + expect(service.find_or_fetch_entity).to be_nil + end + end + end +end From 80dfdcd6afb600b400c7bccd427918658feebdf3 Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Mon, 11 Jun 2018 18:21:57 +0300 Subject: [PATCH 3/4] Add web+diaspora:// links handler installation --- app/assets/javascripts/main.js | 1 + app/assets/javascripts/protocol-handler.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 app/assets/javascripts/protocol-handler.js diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 8c139dc40..563bbc315 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -46,3 +46,4 @@ //= require bootstrap-markdown/bootstrap-markdown //= require helpers/markdown_editor //= require jquery.are-you-sure +//= require protocol-handler diff --git a/app/assets/javascripts/protocol-handler.js b/app/assets/javascripts/protocol-handler.js new file mode 100644 index 000000000..6afedf409 --- /dev/null +++ b/app/assets/javascripts/protocol-handler.js @@ -0,0 +1,17 @@ +// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later + +function registerDiasporaLinksProtocol() { + var protocol = location.protocol; + var slashes = protocol.concat("//"); + var host = slashes.concat(window.location.hostname); + + if (location.port) { + host = host.concat(":" + location.port); + } + + window.navigator.registerProtocolHandler("web+diaspora", host.concat("/link?q=%s"), document.title); +} + +if (typeof (window.navigator.registerProtocolHandler) === "function") { + registerDiasporaLinksProtocol(); +} From a7cc0645e08b18aed2af9b091387794637046613 Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Tue, 26 Jun 2018 23:09:20 +0300 Subject: [PATCH 4/4] Cuke for web+diaspora:// link handler closes #7826 --- Changelog.md | 1 + features/desktop/diaspora_links_resolve.feature | 15 +++++++++++++++ features/step_definitions/link_steps.rb | 7 +++++++ 3 files changed, 23 insertions(+) create mode 100644 features/desktop/diaspora_links_resolve.feature create mode 100644 features/step_definitions/link_steps.rb diff --git a/Changelog.md b/Changelog.md index 7160afc43..1f39b07af 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ * Add compatibility with macOS to `script/configure_bundler` [#7830](https://github.com/diaspora/diaspora/pull/7830) ## Features +* Add `web+diaspora://` link handler [#7826](https://github.com/diaspora/diaspora/pull/7826) # 0.7.6.0 diff --git a/features/desktop/diaspora_links_resolve.feature b/features/desktop/diaspora_links_resolve.feature new file mode 100644 index 000000000..6c615d5d1 --- /dev/null +++ b/features/desktop/diaspora_links_resolve.feature @@ -0,0 +1,15 @@ +@javascript +Feature: (web+)diaspora:// links resolve + In order to open diaspora posts on my pod from external websites + As a user + I want external links to be resolved to local pod paths + + Background: + Given following user exists: + | username | email | + | Alice | alice@alice.alice | + And "alice@alice.alice" has a public post with text "This is a post accessed by an external link" + + Scenario: Resolving web+diaspora:// link + When I open an external link to the first post of "alice@alice.alice" + Then I should see "This is a post accessed by an external link" diff --git a/features/step_definitions/link_steps.rb b/features/step_definitions/link_steps.rb new file mode 100644 index 000000000..122d20eb7 --- /dev/null +++ b/features/step_definitions/link_steps.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +When /^I open an external link to the first post of "([^"]*)"$/ do |email| + user = User.find_by(email: email) + post = user.posts.first + visit(link_path(q: "web+diaspora://#{user.diaspora_handle}/post/#{post.guid}")) +end