diaspora_federation 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +18 -0
- data/README.md +18 -0
- data/Rakefile +26 -0
- data/app/controllers/diaspora_federation/application_controller.rb +6 -0
- data/app/controllers/diaspora_federation/h_card_controller.rb +20 -0
- data/app/controllers/diaspora_federation/receive_controller.rb +35 -0
- data/app/controllers/diaspora_federation/webfinger_controller.rb +60 -0
- data/config/routes.rb +15 -0
- data/lib/diaspora_federation.rb +120 -0
- data/lib/diaspora_federation/engine.rb +15 -0
- data/lib/diaspora_federation/logging.rb +25 -0
- data/lib/diaspora_federation/version.rb +5 -0
- data/lib/diaspora_federation/web_finger.rb +14 -0
- data/lib/diaspora_federation/web_finger/exceptions.rb +19 -0
- data/lib/diaspora_federation/web_finger/h_card.rb +319 -0
- data/lib/diaspora_federation/web_finger/host_meta.rb +100 -0
- data/lib/diaspora_federation/web_finger/web_finger.rb +263 -0
- data/lib/diaspora_federation/web_finger/xrd_document.rb +181 -0
- data/lib/tasks/diaspora_federation_tasks.rake +4 -0
- data/lib/tasks/tests.rake +18 -0
- metadata +94 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 35ffb28db27478325eaf2f2d61290b125f2e20e2
|
4
|
+
data.tar.gz: cfaca21b0289d178044895bbb91326b51520fed7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0ef15d9756b8174572d54d926388e8dffbeabea94f342ceb62ef536f5c52f6c9003e904b208c22a277c4c4906c1723ea7bf7f82181dae3726a9b5aeaa51b77a0
|
7
|
+
data.tar.gz: 1e77ba30e7745f64927b58ab0c110fa9467f0b8981e539ce24500754d5a61f548ac63e362c84ad5cfb7b6bbbca92ac065b1aa1e4a9debf1599150dbad27a1f5b
|
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Diaspora Federation Rails Engine
|
2
|
+
Copyright (C) 2015 Benjamin Neff
|
3
|
+
|
4
|
+
This program is free software: you can redistribute it and/or modify
|
5
|
+
it under the terms of the GNU Affero General Public License as
|
6
|
+
published by the Free Software Foundation, either version 3 of the
|
7
|
+
License, or (at your option) any later version.
|
8
|
+
|
9
|
+
This program is distributed in the hope that it will be useful,
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
GNU Affero General Public License for more details.
|
13
|
+
|
14
|
+
You should have received a copy of the GNU Affero General Public License
|
15
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
Some parts are based on an older federation gem from Florian Staudacher:
|
18
|
+
https://github.com/Raven24/diaspora-federation
|
data/README.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# diaspora* federation rails engine
|
2
|
+
|
3
|
+
#### A rails engine that adds the diaspora* federation protocol to a rails app
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/SuperTux88/diaspora_federation.svg?branch=master)](https://travis-ci.org/SuperTux88/diaspora_federation)
|
6
|
+
[![Code Climate](https://codeclimate.com/github/SuperTux88/diaspora_federation/badges/gpa.svg)](https://codeclimate.com/github/SuperTux88/diaspora_federation)
|
7
|
+
[![Test Coverage](https://codeclimate.com/github/SuperTux88/diaspora_federation/badges/coverage.svg)](https://codeclimate.com/github/SuperTux88/diaspora_federation/coverage)
|
8
|
+
[![Dependency Status](https://gemnasium.com/SuperTux88/diaspora_federation.svg)](https://gemnasium.com/SuperTux88/diaspora_federation)
|
9
|
+
[![Inline docs](https://inch-ci.org/github/SuperTux88/diaspora_federation.svg?branch=master)](https://inch-ci.org/github/SuperTux88/diaspora_federation)
|
10
|
+
|
11
|
+
[Documentation](http://www.rubydoc.info/github/SuperTux88/diaspora_federation/master) |
|
12
|
+
[Project site](https://diasporafoundation.org) |
|
13
|
+
[Wiki](https://wiki.diasporafoundation.org) |
|
14
|
+
[Bugtracker](https://github.com/SuperTux88/diaspora_federation/issues)
|
15
|
+
|
16
|
+
## License
|
17
|
+
|
18
|
+
This gem is published under the terms of the "GNU Affero General Public License". See the LICENSE file for the exact wording.
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
begin
|
2
|
+
require "bundler/setup"
|
3
|
+
rescue LoadError
|
4
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
5
|
+
end
|
6
|
+
|
7
|
+
require "rdoc/task"
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = "rdoc"
|
11
|
+
rdoc.title = "DiasporaFederation"
|
12
|
+
rdoc.options << "--line-numbers"
|
13
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
14
|
+
end
|
15
|
+
|
16
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
17
|
+
load "rails/tasks/engine.rake"
|
18
|
+
|
19
|
+
load "rails/tasks/statistics.rake"
|
20
|
+
|
21
|
+
Bundler::GemHelper.install_tasks
|
22
|
+
|
23
|
+
Rails.application.load_tasks
|
24
|
+
|
25
|
+
task test: %w(spec:prepare spec)
|
26
|
+
task default: :test
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_dependency "diaspora_federation/application_controller"
|
2
|
+
|
3
|
+
module DiasporaFederation
|
4
|
+
##
|
5
|
+
# this controller generates the hcard
|
6
|
+
class HCardController < ApplicationController
|
7
|
+
##
|
8
|
+
# returns the hcard of the user
|
9
|
+
#
|
10
|
+
# GET /hcard/users/:guid
|
11
|
+
def hcard
|
12
|
+
person = DiasporaFederation.person_class.find_local_by_guid(params[:guid])
|
13
|
+
|
14
|
+
return render nothing: true, status: 404 if person.nil?
|
15
|
+
|
16
|
+
logger.info "hcard profile request for: #{person.diaspora_handle}"
|
17
|
+
render html: WebFinger::HCard.from_profile(person.hcard_profile_hash).to_html.html_safe
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_dependency "diaspora_federation/application_controller"
|
2
|
+
|
3
|
+
module DiasporaFederation
|
4
|
+
##
|
5
|
+
# this controller processes receiving messages
|
6
|
+
class ReceiveController < ApplicationController
|
7
|
+
before_action :check_for_xml
|
8
|
+
|
9
|
+
##
|
10
|
+
# receives public messages
|
11
|
+
#
|
12
|
+
# POST /receive/public
|
13
|
+
def public
|
14
|
+
logger.info "received a public message"
|
15
|
+
logger.debug CGI.unescape(params[:xml])
|
16
|
+
render nothing: true, status: :ok
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# receives private messages for a user
|
21
|
+
#
|
22
|
+
# POST /receive/users/:guid
|
23
|
+
def private
|
24
|
+
logger.info "received a private message for #{params[:guid]}"
|
25
|
+
logger.debug CGI.unescape(params[:xml])
|
26
|
+
render nothing: true, status: :ok
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def check_for_xml
|
32
|
+
render nothing: true, status: 422 if params[:xml].nil?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_dependency "diaspora_federation/application_controller"
|
2
|
+
|
3
|
+
module DiasporaFederation
|
4
|
+
##
|
5
|
+
# this controller handles all webfinger-specific requests
|
6
|
+
class WebfingerController < ApplicationController
|
7
|
+
##
|
8
|
+
# returns the host-meta xml
|
9
|
+
#
|
10
|
+
# example:
|
11
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
12
|
+
# <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
13
|
+
# <Link rel="lrdd" type="application/xrd+xml" template="https://server.example/webfinger?q={uri}"/>
|
14
|
+
# </XRD>
|
15
|
+
#
|
16
|
+
# GET /.well-known/host-meta
|
17
|
+
def host_meta
|
18
|
+
render body: WebfingerController.host_meta_xml, content_type: "application/xrd+xml"
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# @deprecated this is the pre RFC 7033 webfinger
|
23
|
+
#
|
24
|
+
# example:
|
25
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
26
|
+
# <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
27
|
+
# <Subject>acct:alice@localhost:3000</Subject>
|
28
|
+
# <Alias>http://localhost:3000/people/c8e87290f6a20132963908fbffceb188</Alias>
|
29
|
+
# <Link rel="http://microformats.org/profile/hcard" type="text/html" href="http://localhost:3000/hcard/users/c8e87290f6a20132963908fbffceb188"/>
|
30
|
+
# <Link rel="http://joindiaspora.com/seed_location" type="text/html" href="http://localhost:3000/"/>
|
31
|
+
# <Link rel="http://joindiaspora.com/guid" type="text/html" href="c8e87290f6a20132963908fbffceb188"/>
|
32
|
+
# <Link rel="http://webfinger.net/rel/profile-page" type="text/html" href="http://localhost:3000/u/alice"/>
|
33
|
+
# <Link rel="http://schemas.google.com/g/2010#updates-from" type="application/atom+xml" href="http://localhost:3000/public/alice.atom"/>
|
34
|
+
# <Link rel="salmon" href="http://localhost:3000/receive/users/c8e87290f6a20132963908fbffceb188"/>
|
35
|
+
# <Link rel="diaspora-public-key" type="RSA" href="LS0tLS1CRU......"/>
|
36
|
+
# </XRD>
|
37
|
+
# GET /webfinger?q=<uri>
|
38
|
+
def legacy_webfinger
|
39
|
+
person = find_person(params[:q]) if params[:q]
|
40
|
+
|
41
|
+
return render nothing: true, status: 404 if person.nil?
|
42
|
+
|
43
|
+
logger.info "webfinger profile request for: #{person.diaspora_handle}"
|
44
|
+
render body: WebFinger::WebFinger.from_person(person.webfinger_hash).to_xml, content_type: "application/xrd+xml"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
##
|
50
|
+
# creates the host-meta xml with the configured server_uri and caches it
|
51
|
+
# @return [String] XML string
|
52
|
+
def self.host_meta_xml
|
53
|
+
@host_meta_xml ||= WebFinger::HostMeta.from_base_url(DiasporaFederation.server_uri.to_s).to_xml
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_person(query)
|
57
|
+
DiasporaFederation.person_class.find_local_by_diaspora_handle(query.strip.downcase.gsub("acct:", ""))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
DiasporaFederation::Engine.routes.draw do
|
2
|
+
controller :receive do
|
3
|
+
post "receive-new/public" => :public, :as => "receive_public"
|
4
|
+
post "receive-new/users/:guid" => :private, :as => "receive_private"
|
5
|
+
end
|
6
|
+
|
7
|
+
controller :webfinger do
|
8
|
+
get ".well-known/host-meta" => :host_meta, :as => "host_meta"
|
9
|
+
get "webfinger" => :legacy_webfinger, :as => "legacy_webfinger"
|
10
|
+
end
|
11
|
+
|
12
|
+
controller :h_card do
|
13
|
+
get "hcard/users/:guid" => :hcard, :as => "hcard"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require "diaspora_federation/engine"
|
2
|
+
require "diaspora_federation/logging"
|
3
|
+
|
4
|
+
require "diaspora_federation/web_finger"
|
5
|
+
|
6
|
+
##
|
7
|
+
# diaspora* federation rails engine
|
8
|
+
module DiasporaFederation
|
9
|
+
extend Logging
|
10
|
+
|
11
|
+
class << self
|
12
|
+
##
|
13
|
+
# the pod url
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
# config.server_uri = URI("http://localhost:3000/")
|
17
|
+
# or
|
18
|
+
# config.server_uri = AppConfig.pod_uri
|
19
|
+
attr_accessor :server_uri
|
20
|
+
|
21
|
+
##
|
22
|
+
# the class to use as +Person+
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
# config.person_class = Person.to_s
|
26
|
+
#
|
27
|
+
# This class must have the following methods:
|
28
|
+
#
|
29
|
+
# *find_local_by_diaspora_handle*
|
30
|
+
# This should return a +Person+, which is on this pod and the account is not closed.
|
31
|
+
#
|
32
|
+
# *find_local_by_guid*
|
33
|
+
# This should return a +Person+, which is on this pod and the account is not closed.
|
34
|
+
#
|
35
|
+
# *webfinger_hash*
|
36
|
+
# This should return a +Hash+ with the following information:
|
37
|
+
# {
|
38
|
+
# acct_uri: "acct:user@server.example",
|
39
|
+
# alias_url: "https://server.example/people/0123456789abcdef",
|
40
|
+
# hcard_url: "https://server.example/hcard/users/0123456789abcdef",
|
41
|
+
# seed_url: "https://server.example/",
|
42
|
+
# profile_url: "https://server.example/u/user",
|
43
|
+
# atom_url: "https://server.example/public/user.atom",
|
44
|
+
# salmon_url: "https://server.example/receive/users/0123456789abcdef",
|
45
|
+
# guid: "0123456789abcdef",
|
46
|
+
# pubkey: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----"
|
47
|
+
# }
|
48
|
+
#
|
49
|
+
# *hcard_profile_hash*
|
50
|
+
# This should return a +Hash+ with the following information:
|
51
|
+
# {
|
52
|
+
# guid: "0123456789abcdef",
|
53
|
+
# nickname: "user",
|
54
|
+
# full_name: "User Name",
|
55
|
+
# url: "https://server.example/",
|
56
|
+
# photo_large_url: "https://server.example/uploads/f.jpg",
|
57
|
+
# photo_medium_url: "https://server.example/uploads/m.jpg",
|
58
|
+
# photo_small_url: "https://server.example/uploads/s.jpg",
|
59
|
+
# pubkey: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----",
|
60
|
+
# searchable: true,
|
61
|
+
# first_name: "User",
|
62
|
+
# last_name: "Name"
|
63
|
+
# }
|
64
|
+
attr_accessor :person_class
|
65
|
+
def person_class
|
66
|
+
const_get(@person_class)
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# configure the federation engine
|
71
|
+
#
|
72
|
+
# DiasporaFederation.configure do |config|
|
73
|
+
# config.server_uri = "http://localhost:3000/"
|
74
|
+
# end
|
75
|
+
def configure
|
76
|
+
yield self
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# validates if the engine is configured correctly
|
81
|
+
#
|
82
|
+
# called from after_initialize
|
83
|
+
# @raise [ConfigurationError] if the configuration is incomplete or invalid
|
84
|
+
def validate_config
|
85
|
+
configuration_error "missing server_uri" unless @server_uri.respond_to? :host
|
86
|
+
validate_class(@person_class, "person_class", %i(
|
87
|
+
find_local_by_diaspora_handle
|
88
|
+
find_local_by_guid
|
89
|
+
webfinger_hash
|
90
|
+
hcard_profile_hash
|
91
|
+
))
|
92
|
+
logger.info "successfully configured the federation engine"
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def validate_class(klass, name, methods)
|
98
|
+
configuration_error "missing #{name}" unless klass
|
99
|
+
entity = const_get(klass)
|
100
|
+
|
101
|
+
return logger.warn "table for #{entity} doesn't exist, skip validation" unless entity.table_exists?
|
102
|
+
|
103
|
+
methods.each {|method| entity_respond_to?(entity, name, method) }
|
104
|
+
end
|
105
|
+
|
106
|
+
def entity_respond_to?(entity, name, method)
|
107
|
+
valid = entity.respond_to?(method) || entity.column_names.include?(method.to_s) || entity.method_defined?(method)
|
108
|
+
configuration_error "the configured class #{entity} for #{name} doesn't respond to #{method}" unless valid
|
109
|
+
end
|
110
|
+
|
111
|
+
def configuration_error(message)
|
112
|
+
logger.fatal("diaspora federation configuration error: #{message}")
|
113
|
+
raise ConfigurationError, message
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# raised, if the engine is not configured correctly
|
118
|
+
class ConfigurationError < RuntimeError
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
##
|
3
|
+
# diaspora* federation rails engine
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace DiasporaFederation
|
6
|
+
|
7
|
+
config.generators do |g|
|
8
|
+
g.test_framework :rspec
|
9
|
+
end
|
10
|
+
|
11
|
+
config.after_initialize do
|
12
|
+
DiasporaFederation.validate_config
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
##
|
3
|
+
# logging module for the diaspora federation engine
|
4
|
+
#
|
5
|
+
# it uses the logging-gem if available
|
6
|
+
module Logging
|
7
|
+
private
|
8
|
+
|
9
|
+
##
|
10
|
+
# get the logger for this class
|
11
|
+
#
|
12
|
+
# use the logging-gem if available, else use a default logger
|
13
|
+
def logger
|
14
|
+
@logger ||= begin
|
15
|
+
# use logging-gem if available
|
16
|
+
return ::Logging::Logger[self] if Object.const_defined?("::Logging::Logger")
|
17
|
+
|
18
|
+
# fallback logger
|
19
|
+
@logger = Logger.new(STDOUT)
|
20
|
+
@logger.level = Logger.const_get(Rails.configuration.log_level.to_s.upcase)
|
21
|
+
@logger
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
##
|
3
|
+
# This module provides the namespace for the various classes implementing
|
4
|
+
# WebFinger and other protocols used for metadata discovery on remote servers
|
5
|
+
# in the Diaspora* network.
|
6
|
+
module WebFinger
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require "diaspora_federation/web_finger/exceptions"
|
11
|
+
require "diaspora_federation/web_finger/xrd_document"
|
12
|
+
require "diaspora_federation/web_finger/host_meta"
|
13
|
+
require "diaspora_federation/web_finger/web_finger"
|
14
|
+
require "diaspora_federation/web_finger/h_card"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module WebFinger
|
3
|
+
##
|
4
|
+
# Raised, if the XML structure is invalid
|
5
|
+
class InvalidDocument < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
##
|
9
|
+
# Raised, if something is wrong with the webfinger data
|
10
|
+
#
|
11
|
+
# * if the +webfinger_url+ is missing or malformed in {HostMeta.from_base_url} or {HostMeta.from_xml}
|
12
|
+
# * if the +data+ given to {WebFinger.from_person} is an invalid type or doesn't contain all required entries
|
13
|
+
# * if the parsed XML from {WebFinger.from_xml} is incomplete
|
14
|
+
# * if the params passed to {HCard.from_profile} or {HCard.from_html}
|
15
|
+
# are in some way malformed, invalid or incomplete.
|
16
|
+
class InvalidData < RuntimeError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,319 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module WebFinger
|
3
|
+
##
|
4
|
+
# This class provides the means of generating an parsing account data to and
|
5
|
+
# from the hCard format.
|
6
|
+
# hCard is based on +RFC 2426+ (vCard) which got superseded by +RFC 6350+.
|
7
|
+
# There is a draft for a new h-card format specification, that makes use of
|
8
|
+
# the new vCard standard.
|
9
|
+
#
|
10
|
+
# @note The current implementation contains a huge amount of legacy elements
|
11
|
+
# and classes, that should be removed and cleaned up in later iterations.
|
12
|
+
#
|
13
|
+
# @todo This needs some radical restructuring. The generated HTML is not
|
14
|
+
# correctly nested according to the hCard standard and class names are
|
15
|
+
# partially wrong. Also, apart from that, it's just ugly.
|
16
|
+
#
|
17
|
+
# @example Creating a hCard document from account data
|
18
|
+
# hc = HCard.from_profile({
|
19
|
+
# guid: "0123456789abcdef",
|
20
|
+
# nickname: "user",
|
21
|
+
# full_name: "User Name",
|
22
|
+
# url: "https://server.example/",
|
23
|
+
# photo_large_url: "https://server.example/uploads/l.jpg",
|
24
|
+
# photo_medium_url: "https://server.example/uploads/m.jpg",
|
25
|
+
# photo_small_url: "https://server.example/uploads/s.jpg",
|
26
|
+
# pubkey: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----",
|
27
|
+
# searchable: true,
|
28
|
+
# first_name: "User",
|
29
|
+
# last_name: "Name"
|
30
|
+
# })
|
31
|
+
# html_string = hc.to_html
|
32
|
+
#
|
33
|
+
# @example Create a HCard instance from an hCard document
|
34
|
+
# hc = HCard.from_html(html_string)
|
35
|
+
# ...
|
36
|
+
# full_name = hc.full_name
|
37
|
+
# ...
|
38
|
+
#
|
39
|
+
# @see http://microformats.org/wiki/hCard "hCard 1.0"
|
40
|
+
# @see http://microformats.org/wiki/h-card "h-card" (draft)
|
41
|
+
# @see http://www.ietf.org/rfc/rfc2426.txt "vCard MIME Directory Profile" (obsolete)
|
42
|
+
# @see http://www.ietf.org/rfc/rfc6350.txt "vCard Format Specification"
|
43
|
+
class HCard
|
44
|
+
private_class_method :new
|
45
|
+
|
46
|
+
# This is just the guid. When a user creates an account on a pod, the pod
|
47
|
+
# MUST assign them a guid - a random hexadecimal string of at least 8
|
48
|
+
# hexadecimal digits.
|
49
|
+
# @return [String] guid
|
50
|
+
attr_reader :guid
|
51
|
+
|
52
|
+
# the first part of the diaspora handle
|
53
|
+
# @return [String] nickname
|
54
|
+
attr_reader :nickname
|
55
|
+
|
56
|
+
# @return [String] display name of the user
|
57
|
+
attr_reader :full_name
|
58
|
+
|
59
|
+
# @deprecated should be changed to the profile url. The pod url is in
|
60
|
+
# the WebFinger (see {WebFinger#seed_url}, will affect older Diaspora*
|
61
|
+
# installations).
|
62
|
+
#
|
63
|
+
# @return [String] link to the pod
|
64
|
+
attr_reader :url
|
65
|
+
|
66
|
+
# When a user is created on the pod, the pod MUST generate a pgp keypair
|
67
|
+
# for them. This key is used for signing messages. The format is a
|
68
|
+
# DER-encoded PKCS#1 key beginning with the text
|
69
|
+
# "-----BEGIN PUBLIC KEY-----" and ending with "-----END PUBLIC KEY-----".
|
70
|
+
# @return [String] public key
|
71
|
+
attr_reader :pubkey
|
72
|
+
|
73
|
+
# @return [String] url to the big avatar (300x300)
|
74
|
+
attr_reader :photo_large_url
|
75
|
+
# @return [String] url to the medium avatar (100x100)
|
76
|
+
attr_reader :photo_medium_url
|
77
|
+
# @return [String] url to the small avatar (50x50)
|
78
|
+
attr_reader :photo_small_url
|
79
|
+
|
80
|
+
# @deprecated We decided to only use one name field, these should be removed
|
81
|
+
# in later iterations (will affect older Diaspora* installations).
|
82
|
+
#
|
83
|
+
# @see #full_name
|
84
|
+
# @return [String] first name
|
85
|
+
attr_reader :first_name
|
86
|
+
|
87
|
+
# @deprecated We decided to only use one name field, these should be removed
|
88
|
+
# in later iterations (will affect older Diaspora* installations).
|
89
|
+
#
|
90
|
+
# @see #full_name
|
91
|
+
# @return [String] last name
|
92
|
+
attr_reader :last_name
|
93
|
+
|
94
|
+
# @deprecated As this is a simple property, consider move to WebFinger instead
|
95
|
+
# of HCard. vCard has no comparable field for this information, but
|
96
|
+
# Webfinger may declare arbitrary properties (will affect older Diaspora*
|
97
|
+
# installations).
|
98
|
+
#
|
99
|
+
# flag if a user is searchable by name
|
100
|
+
# @return [Boolean] searchable flag
|
101
|
+
attr_reader :searchable
|
102
|
+
|
103
|
+
# CSS selectors for finding all the hCard fields
|
104
|
+
SELECTORS = {
|
105
|
+
uid: ".uid",
|
106
|
+
nickname: ".nickname",
|
107
|
+
fn: ".fn",
|
108
|
+
given_name: ".given_name",
|
109
|
+
family_name: ".family_name",
|
110
|
+
url: "#pod_location[href]",
|
111
|
+
photo: ".entity_photo .photo[src]",
|
112
|
+
photo_medium: ".entity_photo_medium .photo[src]",
|
113
|
+
photo_small: ".entity_photo_small .photo[src]",
|
114
|
+
key: ".key",
|
115
|
+
searchable: ".searchable"
|
116
|
+
}
|
117
|
+
|
118
|
+
# Create the HTML string from the current HCard instance
|
119
|
+
# @return [String] HTML string
|
120
|
+
def to_html
|
121
|
+
builder = create_builder
|
122
|
+
|
123
|
+
content = builder.doc.at_css("#content_inner")
|
124
|
+
|
125
|
+
add_simple_property(content, :uid, "uid", @guid)
|
126
|
+
add_simple_property(content, :nickname, "nickname", @nickname)
|
127
|
+
add_simple_property(content, :full_name, "fn", @full_name)
|
128
|
+
add_simple_property(content, :searchable, "searchable", @searchable)
|
129
|
+
|
130
|
+
add_property(content, :key) do |html|
|
131
|
+
html.pre(@pubkey.to_s, class: "key")
|
132
|
+
end
|
133
|
+
|
134
|
+
# TODO: remove me! ###################
|
135
|
+
add_simple_property(content, :first_name, "given_name", @first_name)
|
136
|
+
add_simple_property(content, :family_name, "family_name", @last_name)
|
137
|
+
#######################################
|
138
|
+
|
139
|
+
add_property(content, :url) do |html|
|
140
|
+
html.a(@url.to_s, id: "pod_location", class: "url", rel: "me", href: @url.to_s)
|
141
|
+
end
|
142
|
+
|
143
|
+
add_photos(content)
|
144
|
+
|
145
|
+
builder.doc.to_xhtml(indent: 2, indent_text: " ")
|
146
|
+
end
|
147
|
+
|
148
|
+
# Creates a new HCard instance from the given Hash containing profile data
|
149
|
+
# @param [Hash] data account data
|
150
|
+
# @return [HCard] HCard instance
|
151
|
+
# @raise [InvalidData] if the account data Hash is invalid or incomplete
|
152
|
+
def self.from_profile(data)
|
153
|
+
raise InvalidData unless account_data_complete?(data)
|
154
|
+
|
155
|
+
hc = allocate
|
156
|
+
hc.instance_eval {
|
157
|
+
@guid = data[:guid]
|
158
|
+
@nickname = data[:nickname]
|
159
|
+
@full_name = data[:full_name]
|
160
|
+
@url = data[:url]
|
161
|
+
@photo_large_url = data[:photo_large_url]
|
162
|
+
@photo_medium_url = data[:photo_medium_url]
|
163
|
+
@photo_small_url = data[:photo_small_url]
|
164
|
+
@pubkey = data[:pubkey]
|
165
|
+
@searchable = data[:searchable]
|
166
|
+
|
167
|
+
# TODO: remove me! ###################
|
168
|
+
@first_name = data[:first_name]
|
169
|
+
@last_name = data[:last_name]
|
170
|
+
#######################################
|
171
|
+
}
|
172
|
+
hc
|
173
|
+
end
|
174
|
+
|
175
|
+
# Creates a new HCard instance from the given HTML string.
|
176
|
+
# @param html_string [String] HTML string
|
177
|
+
# @return [HCard] HCard instance
|
178
|
+
# @raise [InvalidData] if the HTML string is invalid or incomplete
|
179
|
+
def self.from_html(html_string)
|
180
|
+
doc = parse_html_and_validate(html_string)
|
181
|
+
|
182
|
+
hc = allocate
|
183
|
+
hc.instance_eval {
|
184
|
+
@guid = content_from_doc(doc, :uid)
|
185
|
+
@nickname = content_from_doc(doc, :nickname)
|
186
|
+
@full_name = content_from_doc(doc, :fn)
|
187
|
+
@url = element_from_doc(doc, :url)["href"]
|
188
|
+
@photo_large_url = photo_from_doc(doc, :photo)
|
189
|
+
@photo_medium_url = photo_from_doc(doc, :photo_medium)
|
190
|
+
@photo_small_url = photo_from_doc(doc, :photo_small)
|
191
|
+
@pubkey = content_from_doc(doc, :key) unless element_from_doc(doc, :key).nil?
|
192
|
+
@searchable = content_from_doc(doc, :searchable) == "true"
|
193
|
+
|
194
|
+
# TODO: change me! ###################
|
195
|
+
@first_name = content_from_doc(doc, :given_name)
|
196
|
+
@last_name = content_from_doc(doc, :family_name)
|
197
|
+
#######################################
|
198
|
+
}
|
199
|
+
hc
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
# Creates the base HCard html structure
|
205
|
+
# @return [Nokogiri::HTML::Builder] HTML Builder instance
|
206
|
+
def create_builder
|
207
|
+
Nokogiri::HTML::Builder.new do |html|
|
208
|
+
html.html {
|
209
|
+
html.head {
|
210
|
+
html.meta(charset: "UTF-8")
|
211
|
+
html.title(@full_name)
|
212
|
+
}
|
213
|
+
|
214
|
+
html.body {
|
215
|
+
html.div(id: "content") {
|
216
|
+
html.h1(@full_name)
|
217
|
+
html.div(id: "content_inner", class: "entity_profile vcard author") {
|
218
|
+
html.h2("User profile")
|
219
|
+
}
|
220
|
+
}
|
221
|
+
}
|
222
|
+
}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Add a property to the hCard document. The element will be added to the given
|
227
|
+
# container element and a "definition list" structure will be created around
|
228
|
+
# it. A Nokogiri::HTML::Builder instance will be passed to the given block,
|
229
|
+
# which should be used to add the element(s) containing the property data.
|
230
|
+
#
|
231
|
+
# @param container [Nokogiri::XML::Element] parent element for added property HTML
|
232
|
+
# @param name [Symbol] property name
|
233
|
+
# @param block [Proc] block returning an element
|
234
|
+
def add_property(container, name, &block)
|
235
|
+
Nokogiri::HTML::Builder.with(container) do |html|
|
236
|
+
html.dl(class: "entity_#{name}") {
|
237
|
+
html.dt(name.to_s.capitalize)
|
238
|
+
html.dd {
|
239
|
+
block.call(html)
|
240
|
+
}
|
241
|
+
}
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Calls {HCard#add_property} for a simple text property.
|
246
|
+
# @param container [Nokogiri::XML::Element] parent element
|
247
|
+
# @param name [Symbol] property name
|
248
|
+
# @param class_name [String] HTML class name
|
249
|
+
# @param value [#to_s] property value
|
250
|
+
# @see HCard#add_property
|
251
|
+
def add_simple_property(container, name, class_name, value)
|
252
|
+
add_property(container, name) do |html|
|
253
|
+
html.span(value.to_s, class: class_name)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Calls {HCard#add_property} to add the photos
|
258
|
+
# @param container [Nokogiri::XML::Element] parent element
|
259
|
+
# @see HCard#add_property
|
260
|
+
def add_photos(container)
|
261
|
+
add_property(container, :photo) do |html|
|
262
|
+
html.img(class: "photo avatar", width: "300", height: "300", src: @photo_large_url.to_s)
|
263
|
+
end
|
264
|
+
|
265
|
+
add_property(container, :photo_medium) do |html|
|
266
|
+
html.img(class: "photo avatar", width: "100", height: "100", src: @photo_medium_url.to_s)
|
267
|
+
end
|
268
|
+
|
269
|
+
add_property(container, :photo_small) do |html|
|
270
|
+
html.img(class: "photo avatar", width: "50", height: "50", src: @photo_small_url.to_s)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Checks the given account data Hash for correct type and completeness.
|
275
|
+
# @param [Hash] data account data
|
276
|
+
# @return [Boolean] validation result
|
277
|
+
def self.account_data_complete?(data)
|
278
|
+
data.instance_of?(Hash) &&
|
279
|
+
%i(
|
280
|
+
guid nickname full_name url
|
281
|
+
photo_large_url photo_medium_url photo_small_url
|
282
|
+
pubkey searchable first_name last_name
|
283
|
+
).all? {|k| data.key? k }
|
284
|
+
end
|
285
|
+
private_class_method :account_data_complete?
|
286
|
+
|
287
|
+
# Make sure some of the most important elements are present in the parsed
|
288
|
+
# HTML document.
|
289
|
+
# @param [LibXML::XML::Document] doc HTML document
|
290
|
+
# @return [Boolean] validation result
|
291
|
+
def self.html_document_complete?(doc)
|
292
|
+
!(doc.at_css(SELECTORS[:fn]).nil? || doc.at_css(SELECTORS[:nickname]).nil? ||
|
293
|
+
doc.at_css(SELECTORS[:url]).nil? || doc.at_css(SELECTORS[:photo]).nil?)
|
294
|
+
end
|
295
|
+
private_class_method :html_document_complete?
|
296
|
+
|
297
|
+
def self.parse_html_and_validate(html_string)
|
298
|
+
raise ArgumentError, "hcard html is not a string" unless html_string.instance_of?(String)
|
299
|
+
|
300
|
+
doc = Nokogiri::HTML::Document.parse(html_string)
|
301
|
+
raise InvalidData, "hcard html incomplete" unless html_document_complete?(doc)
|
302
|
+
doc
|
303
|
+
end
|
304
|
+
private_class_method :parse_html_and_validate
|
305
|
+
|
306
|
+
def element_from_doc(doc, selector)
|
307
|
+
doc.at_css(SELECTORS[selector])
|
308
|
+
end
|
309
|
+
|
310
|
+
def content_from_doc(doc, content_selector)
|
311
|
+
element_from_doc(doc, content_selector).content
|
312
|
+
end
|
313
|
+
|
314
|
+
def photo_from_doc(doc, photo_selector)
|
315
|
+
element_from_doc(doc, photo_selector)["src"]
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|