add hcard generator/parser from raven24's gem

This commit is contained in:
Benjamin Neff 2015-06-22 03:53:56 +02:00
parent 40cc7d8229
commit 749999a377
6 changed files with 527 additions and 2 deletions

View file

@ -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"

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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