add hcard generator/parser from raven24's gem
This commit is contained in:
parent
40cc7d8229
commit
749999a377
6 changed files with 527 additions and 2 deletions
|
|
@ -11,3 +11,4 @@ require "diaspora_federation/web_finger/exceptions"
|
|||
require "diaspora_federation/web_finger/xrd_document"
|
||||
require "diaspora_federation/web_finger/host_meta"
|
||||
require "diaspora_federation/web_finger/web_finger"
|
||||
require "diaspora_federation/web_finger/h_card"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ module DiasporaFederation
|
|||
# * if the +webfinger_url+ is missing or malformed in {HostMeta.from_base_url} or {HostMeta.from_xml}
|
||||
# * if the +data+ given to {WebFinger.from_person} is an invalid type or doesn't contain all required entries
|
||||
# * if the parsed XML from {WebFinger.from_xml} is incomplete
|
||||
# * if the params passed to {HCard.from_account} or {HCard.from_html}
|
||||
# are in some way malformed, invalid or incomplete.
|
||||
class InvalidData < RuntimeError
|
||||
end
|
||||
end
|
||||
|
|
|
|||
272
lib/diaspora_federation/web_finger/h_card.rb
Normal file
272
lib/diaspora_federation/web_finger/h_card.rb
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
module DiasporaFederation
|
||||
module WebFinger
|
||||
##
|
||||
# This class provides the means of generating an parsing account data to and
|
||||
# from the hCard format.
|
||||
# hCard is based on +RFC 2426+ (vCard) which got superseded by +RFC 6350+.
|
||||
# There is a draft for a new h-card format specification, that makes use of
|
||||
# the new vCard standard.
|
||||
#
|
||||
# @note The current implementation contains a huge amount of legacy elements
|
||||
# and classes, that should be removed and cleaned up in later iterations.
|
||||
#
|
||||
# @todo This needs some radical restructuring. The generated HTML is not
|
||||
# correctly nested according to the hCard standard and class names are
|
||||
# partially wrong. Also, apart from that, it's just ugly.
|
||||
#
|
||||
# @example Creating a hCard document from account data
|
||||
# hc = HCard.from_profile({
|
||||
# guid: "0123456789abcdef",
|
||||
# diaspora_handle: "user@server.example",
|
||||
# full_name: "User Name",
|
||||
# url: "https://server.example/",
|
||||
# photo_full_url: "https://server.example/uploads/f.jpg",
|
||||
# photo_medium_url: "https://server.example/uploads/m.jpg",
|
||||
# photo_small_url: "https://server.example/uploads/s.jpg",
|
||||
# pubkey: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----",
|
||||
# searchable: true,
|
||||
# first_name: "User",
|
||||
# last_name: "Name"
|
||||
# })
|
||||
# html_string = hc.to_html
|
||||
#
|
||||
# @example Create a HCard instance from an hCard document
|
||||
# hc = HCard.from_html(html_string)
|
||||
# ...
|
||||
# full_name = hc.full_name
|
||||
# ...
|
||||
#
|
||||
# @see http://microformats.org/wiki/hCard "hCard 1.0"
|
||||
# @see http://microformats.org/wiki/h-card "h-card" (draft)
|
||||
# @see http://www.ietf.org/rfc/rfc2426.txt "vCard MIME Directory Profile" (obsolete)
|
||||
# @see http://www.ietf.org/rfc/rfc6350.txt "vCard Format Specification"
|
||||
class HCard
|
||||
private_class_method :new
|
||||
|
||||
attr_reader :guid, :nickname, :full_name, :url, :pubkey,
|
||||
:photo_full_url, :photo_medium_url, :photo_small_url
|
||||
|
||||
# @deprecated We decided to only use one name field, these should be removed
|
||||
# in later iterations (will affect older Diaspora* installations).
|
||||
attr_reader :first_name, :last_name
|
||||
|
||||
# @deprecated As this is a simple property, consider move to WebFinger instead
|
||||
# of HCard. vCard has no comparable field for this information, but
|
||||
# Webfinger may declare arbitrary properties (will affect older Diaspora*
|
||||
# installations).
|
||||
attr_reader :searchable
|
||||
|
||||
# CSS selectors for finding all the hCard fields
|
||||
SELECTORS = {
|
||||
uid: ".uid",
|
||||
nickname: ".nickname",
|
||||
fn: ".fn",
|
||||
given_name: ".given_name",
|
||||
family_name: ".family_name",
|
||||
url: "#pod_location[href]",
|
||||
photo: ".entity_photo .photo[src]",
|
||||
photo_medium: ".entity_photo_medium .photo[src]",
|
||||
photo_small: ".entity_photo_small .photo[src]",
|
||||
key: ".key",
|
||||
searchable: ".searchable"
|
||||
}
|
||||
|
||||
# Create the HTML string from the current HCard instance
|
||||
# @return [String] HTML string
|
||||
def to_html
|
||||
builder = create_builder
|
||||
|
||||
content = builder.doc.at_css("#content_inner")
|
||||
|
||||
add_simple_property(content, :uid, "uid", @guid)
|
||||
add_simple_property(content, :nickname, "nickname", @nickname)
|
||||
add_simple_property(content, :full_name, "fn", @full_name)
|
||||
add_simple_property(content, :searchable, "searchable", @searchable)
|
||||
add_simple_property(content, :key, "key", @pubkey)
|
||||
|
||||
# TODO: change me! ###################
|
||||
add_simple_property(content, :first_name, "given_name", @first_name)
|
||||
add_simple_property(content, :family_name, "family_name", @last_name)
|
||||
#######################################
|
||||
|
||||
add_property(content, :url) do |html|
|
||||
html.a(@url.to_s, id: "pod_location", class: "url", rel: "me", href: @url.to_s)
|
||||
end
|
||||
|
||||
add_photos(content)
|
||||
|
||||
builder.doc.to_xhtml(indent: 2, indent_text: " ")
|
||||
end
|
||||
|
||||
# Creates a new HCard instance from the given Hash containing profile data
|
||||
# @param [Hash] data account data
|
||||
# @return [HCard] HCard instance
|
||||
# @raise [InvalidData] if the account data Hash is invalid or incomplete
|
||||
def self.from_profile(data)
|
||||
raise InvalidData unless account_data_complete?(data)
|
||||
|
||||
hc = allocate
|
||||
hc.instance_eval {
|
||||
@guid = data[:guid]
|
||||
@nickname = data[:diaspora_handle].split("@").first
|
||||
@full_name = data[:full_name]
|
||||
@url = data[:url]
|
||||
@photo_full_url = data[:photo_full_url]
|
||||
@photo_medium_url = data[:photo_medium_url]
|
||||
@photo_small_url = data[:photo_small_url]
|
||||
@pubkey = data[:pubkey]
|
||||
@searchable = data[:searchable]
|
||||
|
||||
# TODO: change me! ###################
|
||||
@first_name = data[:first_name]
|
||||
@last_name = data[:last_name]
|
||||
#######################################
|
||||
}
|
||||
hc
|
||||
end
|
||||
|
||||
# Creates a new HCard instance from the given HTML string.
|
||||
# @param html_string [String] HTML string
|
||||
# @return [HCard] HCard instance
|
||||
# @raise [InvalidData] if the HTML string is invalid or incomplete
|
||||
def self.from_html(html_string)
|
||||
doc = parse_html_and_validate(html_string)
|
||||
|
||||
hc = allocate
|
||||
hc.instance_eval {
|
||||
@guid = content_from_doc(doc, :uid)
|
||||
@nickname = content_from_doc(doc, :nickname)
|
||||
@full_name = content_from_doc(doc, :fn)
|
||||
@url = element_from_doc(doc, :url)["href"]
|
||||
@photo_full_url = photo_from_doc(doc, :photo)
|
||||
@photo_medium_url = photo_from_doc(doc, :photo_medium)
|
||||
@photo_small_url = photo_from_doc(doc, :photo_small)
|
||||
@pubkey = content_from_doc(doc, :key) unless element_from_doc(doc, :key).nil?
|
||||
@searchable = content_from_doc(doc, :searchable)
|
||||
|
||||
# TODO: change me! ###################
|
||||
@first_name = content_from_doc(doc, :given_name)
|
||||
@last_name = content_from_doc(doc, :family_name)
|
||||
#######################################
|
||||
}
|
||||
hc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Creates the base HCard html structure
|
||||
# @return [Nokogiri::HTML::Builder] HTML Builder instance
|
||||
def create_builder
|
||||
Nokogiri::HTML::Builder.new do |html|
|
||||
html.html {
|
||||
html.head {
|
||||
html.meta(charset: "UTF-8")
|
||||
html.title(@full_name)
|
||||
}
|
||||
|
||||
html.body {
|
||||
html.div(id: "content") {
|
||||
html.h1(@full_name)
|
||||
html.div(id: "content_inner", class: "entity_profile vcard author") {
|
||||
html.h2("User profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Add a property to the hCard document. The element will be added to the given
|
||||
# container element and a "definition list" structure will be created around
|
||||
# it. A Nokogiri::HTML::Builder instance will be passed to the given block,
|
||||
# which should be used to add the element(s) containing the property data.
|
||||
#
|
||||
# @param container [Nokogiri::XML::Element] parent element for added property HTML
|
||||
# @param name [Symbol] property name
|
||||
# @param block [Proc] block returning an element
|
||||
def add_property(container, name, &block)
|
||||
Nokogiri::HTML::Builder.with(container) do |html|
|
||||
html.dl(class: "entity_#{name}") {
|
||||
html.dt(name.to_s.capitalize)
|
||||
html.dd {
|
||||
block.call(html)
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Calls {HCard#add_property} for a simple text property.
|
||||
# @param container [Nokogiri::XML::Element] parent element
|
||||
# @param name [Symbol] property name
|
||||
# @param class_name [String] HTML class name
|
||||
# @param value [#to_s] property value
|
||||
# @see HCard#add_property
|
||||
def add_simple_property(container, name, class_name, value)
|
||||
add_property(container, name) do |html|
|
||||
html.span(value.to_s, class: class_name)
|
||||
end
|
||||
end
|
||||
|
||||
# Calls {HCard#add_property} to add the photos
|
||||
# @param container [Nokogiri::XML::Element] parent element
|
||||
# @see HCard#add_property
|
||||
def add_photos(container)
|
||||
add_property(container, :photo) do |html|
|
||||
html.img(class: "photo avatar", width: "300", height: "300", src: @photo_full_url.to_s)
|
||||
end
|
||||
|
||||
add_property(container, :photo_medium) do |html|
|
||||
html.img(class: "photo avatar", width: "100", height: "100", src: @photo_medium_url.to_s)
|
||||
end
|
||||
|
||||
add_property(container, :photo_small) do |html|
|
||||
html.img(class: "photo avatar", width: "50", height: "50", src: @photo_small_url.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
# Checks the given account data Hash for correct type and completeness.
|
||||
# @param [Hash] data account data
|
||||
# @return [Boolean] validation result
|
||||
def self.account_data_complete?(data)
|
||||
data.instance_of?(Hash) && data.key?(:guid) &&
|
||||
data.key?(:diaspora_handle) && data.key?(:full_name) &&
|
||||
data.key?(:url) && data.key?(:photo_full_url) &&
|
||||
data.key?(:photo_medium_url) && data.key?(:photo_small_url) &&
|
||||
data.key?(:pubkey) && data.key?(:searchable) &&
|
||||
data.key?(:first_name) && data.key?(:last_name)
|
||||
end
|
||||
private_class_method :account_data_complete?
|
||||
|
||||
# Make sure some of the most important elements are present in the parsed
|
||||
# HTML document.
|
||||
# @param [LibXML::XML::Document] doc HTML document
|
||||
# @return [Boolean] validation result
|
||||
def self.html_document_complete?(doc)
|
||||
!(doc.at_css(SELECTORS[:fn]).nil? || doc.at_css(SELECTORS[:nickname]).nil? ||
|
||||
doc.at_css(SELECTORS[:url]).nil? || doc.at_css(SELECTORS[:photo]).nil?)
|
||||
end
|
||||
private_class_method :html_document_complete?
|
||||
|
||||
def self.parse_html_and_validate(html_string)
|
||||
raise ArgumentError, "hcard html is not a string" unless html_string.instance_of?(String)
|
||||
|
||||
doc = Nokogiri::HTML::Document.parse(html_string)
|
||||
raise InvalidData, "hcard html incomplete" unless html_document_complete?(doc)
|
||||
doc
|
||||
end
|
||||
private_class_method :parse_html_and_validate
|
||||
|
||||
def element_from_doc(doc, selector)
|
||||
doc.at_css(SELECTORS[selector])
|
||||
end
|
||||
|
||||
def content_from_doc(doc, content_selector)
|
||||
element_from_doc(doc, content_selector).content
|
||||
end
|
||||
|
||||
def photo_from_doc(doc, photo_selector)
|
||||
element_from_doc(doc, photo_selector)["src"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -123,7 +123,7 @@ module DiasporaFederation
|
|||
doc.to_xml
|
||||
end
|
||||
|
||||
# Create a WebFinger instance from the given account data Hash.
|
||||
# Create a WebFinger instance from the given person data Hash.
|
||||
# @param [Hash] data account data
|
||||
# @return [WebFinger] WebFinger instance
|
||||
# @raise [InvalidData] if the given data Hash is invalid or incomplete
|
||||
|
|
|
|||
250
spec/lib/web_finger/h_card_spec.rb
Normal file
250
spec/lib/web_finger/h_card_spec.rb
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
module DiasporaFederation
|
||||
describe WebFinger::HCard do
|
||||
let(:guid) { "abcdef0123456789" }
|
||||
let(:handle) { "user@pod.example.tld" }
|
||||
let(:first_name) { "Test" }
|
||||
let(:last_name) { "Testington" }
|
||||
let(:name) { "#{first_name} #{last_name}" }
|
||||
let(:url) { "https://pod.example.tld/users/me" }
|
||||
let(:photo_url) { "https://pod.example.tld/uploads/f.jpg" }
|
||||
let(:photo_url_m) { "https://pod.example.tld/uploads/m.jpg" }
|
||||
let(:photo_url_s) { "https://pod.example.tld/uploads/s.jpg" }
|
||||
let(:key) { "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----" }
|
||||
let(:searchable) { true }
|
||||
|
||||
let(:html) {
|
||||
<<-HTML
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta charset="UTF-8" />
|
||||
<title>#{name}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<h1>#{name}</h1>
|
||||
<div id="content_inner" class="entity_profile vcard author">
|
||||
<h2>User profile</h2>
|
||||
<dl class="entity_uid">
|
||||
<dt>Uid</dt>
|
||||
<dd>
|
||||
<span class="uid">#{guid}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_nickname">
|
||||
<dt>Nickname</dt>
|
||||
<dd>
|
||||
<span class="nickname">#{handle.split('@').first}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_full_name">
|
||||
<dt>Full_name</dt>
|
||||
<dd>
|
||||
<span class="fn">#{name}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_searchable">
|
||||
<dt>Searchable</dt>
|
||||
<dd>
|
||||
<span class="searchable">#{searchable}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_key">
|
||||
<dt>Key</dt>
|
||||
<dd>
|
||||
<span class="key">#{key}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_first_name">
|
||||
<dt>First_name</dt>
|
||||
<dd>
|
||||
<span class="given_name">#{first_name}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_family_name">
|
||||
<dt>Family_name</dt>
|
||||
<dd>
|
||||
<span class="family_name">#{last_name}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_url">
|
||||
<dt>Url</dt>
|
||||
<dd>
|
||||
<a id="pod_location" class="url" rel="me" href="#{url}">#{url}</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_photo">
|
||||
<dt>Photo</dt>
|
||||
<dd>
|
||||
<img class="photo avatar" width="300" height="300" src="#{photo_url}" />
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_photo_medium">
|
||||
<dt>Photo_medium</dt>
|
||||
<dd>
|
||||
<img class="photo avatar" width="100" height="100" src="#{photo_url_m}" />
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_photo_small">
|
||||
<dt>Photo_small</dt>
|
||||
<dd>
|
||||
<img class="photo avatar" width="50" height="50" src="#{photo_url_s}" />
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
}
|
||||
|
||||
it "must not create blank instances" do
|
||||
expect { WebFinger::HCard.new }.to raise_error(NameError)
|
||||
end
|
||||
|
||||
context "generation" do
|
||||
it "creates an instance from a data hash" do
|
||||
hc = WebFinger::HCard.from_profile(
|
||||
guid: guid,
|
||||
diaspora_handle: handle,
|
||||
full_name: name,
|
||||
url: url,
|
||||
photo_full_url: photo_url,
|
||||
photo_medium_url: photo_url_m,
|
||||
photo_small_url: photo_url_s,
|
||||
pubkey: key,
|
||||
searchable: searchable,
|
||||
first_name: first_name,
|
||||
last_name: last_name
|
||||
)
|
||||
expect(hc.to_html).to eq(html)
|
||||
end
|
||||
|
||||
it "fails if some params are missing" do
|
||||
expect {
|
||||
WebFinger::HCard.from_profile(
|
||||
guid: guid,
|
||||
diaspora_handle: handle
|
||||
)
|
||||
}.to raise_error(WebFinger::InvalidData)
|
||||
end
|
||||
|
||||
it "fails if nothing was given" do
|
||||
expect { WebFinger::HCard.from_profile({}) }.to raise_error(WebFinger::InvalidData)
|
||||
end
|
||||
|
||||
it "fails if nil was given" do
|
||||
expect { WebFinger::HCard.from_profile(nil) }.to raise_error(WebFinger::InvalidData)
|
||||
end
|
||||
end
|
||||
|
||||
context "parsing" do
|
||||
it "reads its own output" do
|
||||
hc = WebFinger::HCard.from_html(html)
|
||||
expect(hc.guid).to eq(guid)
|
||||
expect(hc.nickname).to eq(handle.split("@").first)
|
||||
expect(hc.full_name).to eq(name)
|
||||
expect(hc.url).to eq(url)
|
||||
expect(hc.photo_full_url).to eq(photo_url)
|
||||
expect(hc.photo_medium_url).to eq(photo_url_m)
|
||||
expect(hc.photo_small_url).to eq(photo_url_s)
|
||||
expect(hc.pubkey).to eq(key)
|
||||
expect(hc.searchable).to eq(searchable.to_s)
|
||||
|
||||
expect(hc.first_name).to eq(first_name)
|
||||
expect(hc.last_name).to eq(last_name)
|
||||
end
|
||||
|
||||
it "reads old-style HTML" do
|
||||
historic_html = <<-HTML
|
||||
<div id="content">
|
||||
<h1>#{name}</h1>
|
||||
<div id="content_inner">
|
||||
<div class="entity_profile vcard author" id="i">
|
||||
<h2>User profile</h2>
|
||||
<dl class="entity_nickname">
|
||||
<dt>Nickname</dt>
|
||||
<dd>
|
||||
<a class="nickname url uid" href="#{url}" rel="me">#{name}</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_given_name">
|
||||
<dt>First name</dt>
|
||||
<dd>
|
||||
<span class="given_name">#{first_name}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_family_name">
|
||||
<dt>Family name</dt>
|
||||
<dd>
|
||||
<span class="family_name">#{last_name}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_fn">
|
||||
<dt>Full name</dt>
|
||||
<dd>
|
||||
<span class="fn">#{name}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_url">
|
||||
<dt>URL</dt>
|
||||
<dd>
|
||||
<a class="url" href="#{url}" id="pod_location" rel="me">#{url}</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_photo">
|
||||
<dt>Photo</dt>
|
||||
<dd>
|
||||
<img class="photo avatar" height="300px" src="#{photo_url}" width="300px">
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_photo_medium">
|
||||
<dt>Photo</dt>
|
||||
<dd>
|
||||
<img class="photo avatar" height="100px" src="#{photo_url_m}" width="100px">
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_photo_small">
|
||||
<dt>Photo</dt>
|
||||
<dd>
|
||||
<img class="photo avatar" height="50px" src="#{photo_url_s}" width="50px">
|
||||
</dd>
|
||||
</dl>
|
||||
<dl class="entity_searchable">
|
||||
<dt>Searchable</dt>
|
||||
<dd>
|
||||
<span class="searchable">#{searchable}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML
|
||||
|
||||
hc = WebFinger::HCard.from_html(historic_html)
|
||||
expect(hc.url).to eq(url)
|
||||
expect(hc.photo_full_url).to eq(photo_url)
|
||||
expect(hc.photo_medium_url).to eq(photo_url_m)
|
||||
expect(hc.photo_small_url).to eq(photo_url_s)
|
||||
expect(hc.searchable).to eq(searchable.to_s)
|
||||
|
||||
expect(hc.first_name).to eq(first_name)
|
||||
expect(hc.last_name).to eq(last_name)
|
||||
end
|
||||
|
||||
it "fails if the document is incomplete" do
|
||||
invalid_html = <<-HTML
|
||||
<div id="content">
|
||||
<span class="fn">#{name}</span>
|
||||
</div>
|
||||
HTML
|
||||
expect { WebFinger::HCard.from_html(invalid_html) }.to raise_error(WebFinger::InvalidData)
|
||||
end
|
||||
|
||||
it "fails if the document is not HTML" do
|
||||
expect { WebFinger::HCard.from_html("") }.to raise_error(WebFinger::InvalidData)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -27,7 +27,7 @@ module DiasporaFederation
|
|||
XML
|
||||
|
||||
it "must not create blank instances" do
|
||||
expect { WebFinger::WebFinger.new }.to raise_error
|
||||
expect { WebFinger::WebFinger.new }.to raise_error(NameError)
|
||||
end
|
||||
|
||||
context "generation" do
|
||||
|
|
|
|||
Loading…
Reference in a new issue