diaspora_federation 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,6 @@
1
+ module DiasporaFederation
2
+ ##
3
+ # Base-Controller for all DiasporaFederation-Controller
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -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,5 @@
1
+ module DiasporaFederation
2
+ ##
3
+ # the gem version
4
+ VERSION = "0.0.1"
5
+ 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