ape 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +1 -0
- data/LICENSE +19 -0
- data/Manifest +45 -0
- data/README +12 -0
- data/ape.gemspec +57 -0
- data/bin/ape_server +28 -0
- data/lib/ape.rb +982 -0
- data/lib/ape/atomURI.rb +73 -0
- data/lib/ape/auth/google_login_credentials.rb +96 -0
- data/lib/ape/auth/wsse_credentials.rb +25 -0
- data/lib/ape/authent.rb +42 -0
- data/lib/ape/categories.rb +95 -0
- data/lib/ape/collection.rb +51 -0
- data/lib/ape/crumbs.rb +39 -0
- data/lib/ape/entry.rb +151 -0
- data/lib/ape/escaper.rb +29 -0
- data/lib/ape/feed.rb +117 -0
- data/lib/ape/handler.rb +34 -0
- data/lib/ape/html.rb +17 -0
- data/lib/ape/invoker.rb +54 -0
- data/lib/ape/invokers/deleter.rb +31 -0
- data/lib/ape/invokers/getter.rb +80 -0
- data/lib/ape/invokers/poster.rb +57 -0
- data/lib/ape/invokers/putter.rb +46 -0
- data/lib/ape/layout/ape.css +56 -0
- data/lib/ape/layout/ape_logo.png +0 -0
- data/lib/ape/layout/index.html +54 -0
- data/lib/ape/layout/info.png +0 -0
- data/lib/ape/names.rb +24 -0
- data/lib/ape/print_writer.rb +21 -0
- data/lib/ape/samples.rb +180 -0
- data/lib/ape/samples/atom_schema.txt +338 -0
- data/lib/ape/samples/basic_entry.eruby +16 -0
- data/lib/ape/samples/categories_schema.txt +69 -0
- data/lib/ape/samples/mini_entry.eruby +8 -0
- data/lib/ape/samples/service_schema.txt +187 -0
- data/lib/ape/samples/unclean_xhtml_entry.eruby +21 -0
- data/lib/ape/server.rb +32 -0
- data/lib/ape/service.rb +12 -0
- data/lib/ape/validator.rb +65 -0
- data/lib/ape/version.rb +9 -0
- data/scripts/go.rb +29 -0
- data/test/test_helper.rb +17 -0
- data/test/unit/authent_test.rb +35 -0
- data/test/unit/invoker_test.rb +25 -0
- data/test/unit/samples_test.rb +36 -0
- metadata +111 -0
data/lib/ape/atomURI.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class AtomURI
|
7
|
+
|
8
|
+
def initialize base_uri
|
9
|
+
if base_uri.kind_of? URI
|
10
|
+
@base = base_uri
|
11
|
+
else
|
12
|
+
@base = URI.parse base_uri
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Given a URI pulled out of the middle of an XML doc ('context' provides
|
17
|
+
# containing element) absolutize it if it's relative, with proper regard
|
18
|
+
# for xml:base
|
19
|
+
#
|
20
|
+
def absolutize uri_s, context
|
21
|
+
begin
|
22
|
+
uri = URI.parse uri_s
|
23
|
+
return uri_s if uri.absolute?
|
24
|
+
|
25
|
+
path_base = @base
|
26
|
+
path_to(context).each do |node|
|
27
|
+
if (xb = node.attributes['xml:base'])
|
28
|
+
xb = URI.parse xb
|
29
|
+
if xb.absolute? then path_base = xb else path_base.merge! xb end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
return path_base.merge(uri).to_s
|
34
|
+
rescue URI::InvalidURIError
|
35
|
+
return nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def path_to node
|
40
|
+
if node.class == REXML::Element
|
41
|
+
path_to(node.parent) << node
|
42
|
+
else
|
43
|
+
[ ]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def AtomURI.check(uri_string)
|
48
|
+
if uri_string.kind_of? URI
|
49
|
+
uri = uri_string
|
50
|
+
else
|
51
|
+
begin
|
52
|
+
uri = URI.parse(uri_string)
|
53
|
+
rescue URI::InvalidURIError
|
54
|
+
return "Invalid URI: #{$!}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
unless uri.scheme =~ /^https?$/
|
59
|
+
return "URI scheme must be 'http' or 'https', not '#{uri.scheme}'"
|
60
|
+
else
|
61
|
+
return uri
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def AtomURI.on_the_wire uri
|
66
|
+
if uri.query
|
67
|
+
"#{uri.path}?#{uri.query}"
|
68
|
+
else
|
69
|
+
uri.path
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class GoogleLoginAuthError < StandardError ; end
|
7
|
+
class GoogleLoginAuthUnknownError < GoogleLoginAuthError ; end
|
8
|
+
|
9
|
+
class GoogleLoginCredentials
|
10
|
+
|
11
|
+
GOOGLE_ERROR_MESSAGES = {
|
12
|
+
"BadAuthentication" => "The login request used a username or password that is not recognized.",
|
13
|
+
"NotVerified" => "The account email address has not been verified. The user will need to access their Google account directly to resolve the issue before logging in using a non-Google application.",
|
14
|
+
"TermsNotAgreed" => "The user has not agreed to terms. The user will need to access their Google account directly to resolve the issue before logging in using a non-Google application.",
|
15
|
+
"CaptchaRequired" => "Please visit https://www.google.com/accounts/DisplayUnlockCaptcha to enable access.",
|
16
|
+
"Unknown" => "The error is unknown or unspecified; the request contained invalid input or was malformed.",
|
17
|
+
"AccountDeleted" => "The user account has been deleted.",
|
18
|
+
"AccountDisabled" => "The user account has been disabled.",
|
19
|
+
"ServiceDisabled" => "The user's access to the specified service has been disabled. (The user account may still be valid.)",
|
20
|
+
"ServiceUnavailable" => "The service is not available; try again later.",
|
21
|
+
} unless defined?(GOOGLE_ERROR_MESSAGES)
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@credentials = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_credentials(req, auth, user, password)
|
28
|
+
unless @credentials
|
29
|
+
challenge = parse_www_authenticate(auth)
|
30
|
+
@credentials = googlelogin(username, password, 'ruby-ape-1.0', challenge)
|
31
|
+
end
|
32
|
+
req['Authorization'] = "GoogleLogin auth=#{@credentials}"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def parse_www_authenticate(authenticate)
|
38
|
+
# Returns a dictionary of dictionaries, one dict
|
39
|
+
# per auth-scheme. The dictionary for each auth-scheme
|
40
|
+
# contains all the auth-params.
|
41
|
+
retval = {}
|
42
|
+
authenticate.chomp()
|
43
|
+
# Break off the scheme at the beginning of the line
|
44
|
+
auth_scheme, the_rest = authenticate.split(/ /, 2)
|
45
|
+
# Now loop over all the key value pairs that come after the scheme
|
46
|
+
keyvalues = the_rest.split(/[ ,]/)
|
47
|
+
auth_params = {}
|
48
|
+
keyvalues.each do |keyvalue|
|
49
|
+
if keyvalue.include?("=")
|
50
|
+
key, value = keyvalue.split(/=/, 2)
|
51
|
+
if value.scan(/^\"/).size > 0
|
52
|
+
value = value.strip()[1..-2]
|
53
|
+
end
|
54
|
+
auth_params[key.downcase()] = value.gsub(/\\(.)/, "\\1")
|
55
|
+
elsif keyvalue.size > 0
|
56
|
+
retval[auth_scheme.downcase()] = auth_params
|
57
|
+
auth_scheme = keyvalue
|
58
|
+
auth_params = {}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
retval[auth_scheme.downcase()] = auth_params
|
62
|
+
return retval
|
63
|
+
end
|
64
|
+
|
65
|
+
def googlelogin(name, password, useragent, challenge)
|
66
|
+
service = challenge['googlelogin']['service']
|
67
|
+
|
68
|
+
h = Net::HTTP.new('www.google.com', 443)
|
69
|
+
h.use_ssl = true
|
70
|
+
params = {'Email'=>name, 'Passwd'=>password, 'service'=>service, 'source'=>useragent}
|
71
|
+
data = params.map {|k,v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join('&')
|
72
|
+
res = h.request_post('/accounts/ClientLogin', data, {'Content-Type' => 'application/x-www-form-urlencoded'})
|
73
|
+
d = {}
|
74
|
+
res.body.split(/\n/).each do |keyvalue|
|
75
|
+
key, value = keyvalue.split(/=/)
|
76
|
+
d[key] = value
|
77
|
+
end
|
78
|
+
auth = ""
|
79
|
+
if res == Net::HTTPForbidden
|
80
|
+
if d.has_key?('Error')
|
81
|
+
errorname = d['Error']
|
82
|
+
if GOOGLE_ERROR_MESSAGES.has_key?(errorname)
|
83
|
+
raise GoogleLoginAuthError, GOOGLE_ERROR_MESSAGES[errorname]
|
84
|
+
else
|
85
|
+
raise GoogleLoginAuthUnknownError, errorname
|
86
|
+
end
|
87
|
+
else
|
88
|
+
raise res.error!
|
89
|
+
end
|
90
|
+
else
|
91
|
+
auth = d['Auth']
|
92
|
+
end
|
93
|
+
return auth
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'digest/sha1'
|
3
|
+
|
4
|
+
module Ape
|
5
|
+
class WsseCredentials
|
6
|
+
def initialize
|
7
|
+
@credentials = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_credentials(req, auth, user, password)
|
11
|
+
wsse_auth(user, password) unless @credentials
|
12
|
+
req['X-WSSE'] = @credentials
|
13
|
+
req['Authorization'] = auth
|
14
|
+
end
|
15
|
+
|
16
|
+
def wsse_auth(user, password)
|
17
|
+
nonce = Array.new(10){ rand(0x1000000) }.pack('I*')
|
18
|
+
nonce_b64 = [nonce].pack("m").chomp
|
19
|
+
now = Time.now.gmtime.strftime("%FT%TZ")
|
20
|
+
digest = [Digest::SHA1.digest(nonce_b64 + now + password)].pack("m").chomp
|
21
|
+
|
22
|
+
@credentials = %Q<UsernameToken Username="#{user}", PasswordDigest="#{digest}", Nonce="#{nonce_b64}", Created="#{now}">
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/ape/authent.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
|
4
|
+
module Ape
|
5
|
+
class AuthenticationError < StandardError ; end
|
6
|
+
|
7
|
+
class Authent
|
8
|
+
def initialize(username, password, scheme=nil)
|
9
|
+
@username = username
|
10
|
+
@password = password
|
11
|
+
@auth_plugin = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_to(req, authentication = nil)
|
15
|
+
return unless @username && @password
|
16
|
+
if (authentication)
|
17
|
+
if authentication.strip.downcase.include? 'basic'
|
18
|
+
req.basic_auth @username, @password
|
19
|
+
else
|
20
|
+
@auth_plugin = resolve_plugin(authentication) unless @auth_plugin
|
21
|
+
@auth_plugin.add_credentials(req, authentication, @username, @password)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
req.basic_auth @username, @password
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolve_plugin(authentication)
|
29
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'auth/*.rb')).each do |file|
|
30
|
+
plugin_name = file.gsub(/(.+\/auth\/)(.+)(_credentials.rb)/, '\2').gsub(/_/, '')
|
31
|
+
plugin_class = file.gsub(/(.+\/auth\/)(.+)(.rb)/, '\2').gsub(/(^|_)(.)/) { $2.upcase }
|
32
|
+
|
33
|
+
if (authentication.strip.downcase.include?(plugin_name))
|
34
|
+
return eval("#{plugin_class}.new", binding, __FILE__, __LINE__)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
raise AuthenticationError, "Unknown authentication method: #{authentication}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Dir[File.dirname(__FILE__) + '/auth/*.rb'].each { |l| require l }
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'rexml/xpath'
|
6
|
+
|
7
|
+
module Ape
|
8
|
+
class Categories
|
9
|
+
attr_reader :fixed
|
10
|
+
|
11
|
+
def Categories.from_collection(collection, authent, ape=nil)
|
12
|
+
|
13
|
+
# "catses" because if cats is short for categories, then catses
|
14
|
+
# suggests multiple <app:categories> elements
|
15
|
+
catses = collection.catses
|
16
|
+
|
17
|
+
catses.collect! do |cats|
|
18
|
+
if cats.attribute(:href)
|
19
|
+
getter = Getter.new(cats.attribute(:href).value, authent)
|
20
|
+
if getter.last_error # wonky URI
|
21
|
+
ape.error getter.last_error if ape
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
if !getter.get('application/atomcat+xml')
|
26
|
+
ape.error "Can't fetch categories doc " +
|
27
|
+
"'#{cats.attribute(:href).value}': getter.last_error" if ape
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
ape.warning(getter.last_error) if ape && getter.last_error
|
32
|
+
REXML::Document.new(getter.body).root
|
33
|
+
else
|
34
|
+
# no href attribute
|
35
|
+
cats
|
36
|
+
end
|
37
|
+
end
|
38
|
+
catses.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# Decorate an entry which is about to be posted to a collection with some
|
44
|
+
# categories. For each fixed categories element, pick one of the categories
|
45
|
+
# and add that. If there are no categories elements at all, or if there's
|
46
|
+
# at least one with fixed="no", also add a syntho-cat that we make up.
|
47
|
+
# Return the list of categories that we added.
|
48
|
+
#
|
49
|
+
def Categories.add_cats(entry, collection, authent, ape=nil)
|
50
|
+
|
51
|
+
added = []
|
52
|
+
c = from_collection(collection, authent)
|
53
|
+
if c.empty?
|
54
|
+
add_syntho = true
|
55
|
+
else
|
56
|
+
add_syntho = false
|
57
|
+
|
58
|
+
# for each <app:categories>
|
59
|
+
c.each do |cats|
|
60
|
+
|
61
|
+
default_scheme = cats.attributes['scheme']
|
62
|
+
|
63
|
+
# if it's fixed, pick the first one
|
64
|
+
if cats.attributes['fixed'] == "yes"
|
65
|
+
cat_list = REXML::XPath.match(cats, './atom:category', Names::XmlNamespaces)
|
66
|
+
if cat_list
|
67
|
+
|
68
|
+
# for each <app:category> take the first one whose attribute "term" is not empty
|
69
|
+
cat_list.each do |cat|
|
70
|
+
if cat.attributes['term'].empty?
|
71
|
+
ape.warning 'A mangled category is present in your categories list' if ape
|
72
|
+
else
|
73
|
+
scheme = cat.attributes['scheme']
|
74
|
+
if !scheme
|
75
|
+
scheme = default_scheme
|
76
|
+
end
|
77
|
+
added << entry.add_category(cat.attributes['term'], scheme)
|
78
|
+
break
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
else
|
83
|
+
add_syntho = true
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
if add_syntho
|
90
|
+
added << entry.add_category('simians', 'http://tbray.org/cat-test')
|
91
|
+
end
|
92
|
+
added
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
require 'rexml/xpath'
|
4
|
+
|
5
|
+
module Ape
|
6
|
+
class Collection
|
7
|
+
attr_reader :title, :accept, :href
|
8
|
+
|
9
|
+
def initialize(input, doc_uri = nil)
|
10
|
+
@input = input
|
11
|
+
@accept = []
|
12
|
+
@title = REXML::XPath.first(input, './atom:title', Names::XmlNamespaces)
|
13
|
+
|
14
|
+
# sigh, RNC validation *should* take care of this
|
15
|
+
unless @title
|
16
|
+
raise(SyntaxError, "Collection is missing required 'atom:title'")
|
17
|
+
end
|
18
|
+
@title = @title.texts.join
|
19
|
+
|
20
|
+
if doc_uri
|
21
|
+
uris = AtomURI.new(doc_uri)
|
22
|
+
@href = uris.absolutize(input.attributes['href'], input)
|
23
|
+
else
|
24
|
+
@href = input.attributes['href']
|
25
|
+
end
|
26
|
+
|
27
|
+
# now we have to go looking for the accept
|
28
|
+
@accept = []
|
29
|
+
REXML::XPath.each(input, './app:accept', Names::XmlNamespaces) do |a|
|
30
|
+
@accept << a.texts.join
|
31
|
+
end
|
32
|
+
|
33
|
+
if @accept.empty?
|
34
|
+
@accept = [ Names::AtomEntryMediaType ]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
@input.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_str
|
43
|
+
to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
# the name is supposed to suggest multiple instances of "categories"
|
47
|
+
def catses
|
48
|
+
REXML::XPath.match(@input, './app:categories', Names::XmlNamespaces)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/ape/crumbs.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
|
4
|
+
# All the methods (getter/poster/putter/deleter) use the set_debug_output method
|
5
|
+
# of Net::HTTP to capture the dialogue. This class is what gets passed to
|
6
|
+
# set_debug_output; it exists to define << to save only the interesting bits
|
7
|
+
# of the dialog.
|
8
|
+
#
|
9
|
+
module Ape
|
10
|
+
class Crumbs
|
11
|
+
def initialize
|
12
|
+
@crumbs = []
|
13
|
+
@keep_next = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def grep(pattern)
|
17
|
+
@crumbs.grep(pattern)
|
18
|
+
end
|
19
|
+
|
20
|
+
def << data
|
21
|
+
if @keep_next
|
22
|
+
@crumbs << "> #{data}"
|
23
|
+
@keep_next = false
|
24
|
+
elsif data =~ /^->/
|
25
|
+
@crumbs << "< #{data.gsub(/^.../, '')}"
|
26
|
+
elsif data =~ /^<-/
|
27
|
+
@keep_next = true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def each
|
32
|
+
@crumbs.each { |c| yield c }
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
" " + @crumbs.join("...\n")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/ape/entry.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# Copyright © 2006 Sun Microsystems, Inc. All rights reserved
|
2
|
+
# Use is subject to license terms - see file "LICENSE"
|
3
|
+
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'rexml/xpath'
|
6
|
+
require 'cgi'
|
7
|
+
|
8
|
+
# represents an Atom Entry
|
9
|
+
module Ape
|
10
|
+
class Entry
|
11
|
+
# @element is the REXML dom
|
12
|
+
# @base is the base URI if known
|
13
|
+
|
14
|
+
def initialize(node, uri = nil)
|
15
|
+
if node.class == String
|
16
|
+
@element = REXML::Document.new(node, { :raw => nil }).root
|
17
|
+
else
|
18
|
+
@element = node
|
19
|
+
end
|
20
|
+
if uri
|
21
|
+
@base = AtomURI.new(uri)
|
22
|
+
else
|
23
|
+
@base = nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
"<?xml version='1.0' ?>\n" + @element.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def content_src
|
32
|
+
content = REXML::XPath.first(@element, './atom:content', Names::XmlNamespaces)
|
33
|
+
if content
|
34
|
+
cs = content.attributes['src']
|
35
|
+
cs = @base.absolutize(cs, @element) if @base
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_child(field, namespace = nil)
|
42
|
+
if (namespace)
|
43
|
+
thisNS = {}
|
44
|
+
prefix = 'NN'
|
45
|
+
thisNS[prefix] = namespace
|
46
|
+
else
|
47
|
+
prefix = 'atom'
|
48
|
+
thisNS = Names::XmlNamespaces
|
49
|
+
end
|
50
|
+
xpath = "./#{prefix}:#{field}"
|
51
|
+
return REXML::XPath.first(@element, xpath, thisNS)
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_category(term, scheme = nil, label = nil)
|
55
|
+
c = REXML::Element.new('atom:category', @element)
|
56
|
+
c.add_namespace('atom', Names::AtomNamespace)
|
57
|
+
c.add_attribute('term', term)
|
58
|
+
c.add_attribute('scheme', scheme) if scheme
|
59
|
+
c.add_attribute('label', label) if label
|
60
|
+
c
|
61
|
+
end
|
62
|
+
|
63
|
+
def has_cat(cat)
|
64
|
+
xpath = "./atom:category[@term=\"#{cat.attributes['term']}\""
|
65
|
+
if cat.attributes['scheme']
|
66
|
+
xpath += "and @scheme=\"#{cat.attributes['scheme']}\""
|
67
|
+
end
|
68
|
+
xpath += "]"
|
69
|
+
REXML::XPath.first(@element, xpath, Names::XmlNamespaces)
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete_category(c)
|
73
|
+
@element.delete_element c
|
74
|
+
end
|
75
|
+
|
76
|
+
def child_type(field)
|
77
|
+
n = get_child(field, nil)
|
78
|
+
return nil unless n
|
79
|
+
return n.attributes['type'] || "text"
|
80
|
+
end
|
81
|
+
|
82
|
+
def child_content(field, namespace = nil)
|
83
|
+
n = get_child(field, namespace)
|
84
|
+
return nil unless n
|
85
|
+
|
86
|
+
# if it's type="xhtml", we'll get the content out of the contained
|
87
|
+
# XHTML <div> rather than this element
|
88
|
+
if n.attributes['type'] == 'xhtml'
|
89
|
+
n = REXML::XPath.first(n, "./xhtml:div", Names::XmlNamespaces)
|
90
|
+
unless n
|
91
|
+
return "Error: required xhtml:div child of #{field} is missing"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
text_from n
|
96
|
+
end
|
97
|
+
|
98
|
+
def text_from node
|
99
|
+
text = ''
|
100
|
+
is_html = node.name =~ /(rights|subtitle|summary|title|content)$/ && node.attributes['type'] == 'html'
|
101
|
+
node.find_all do | child |
|
102
|
+
if child.kind_of? REXML::Text
|
103
|
+
v = child.value
|
104
|
+
v = CGI.unescapeHTML(v).gsub(/'/, "'") if is_html
|
105
|
+
text << v
|
106
|
+
elsif child.kind_of? REXML::Element
|
107
|
+
text << text_from(child)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
text
|
111
|
+
end
|
112
|
+
|
113
|
+
def link(rel, ape=nil)
|
114
|
+
l = nil
|
115
|
+
a = REXML::XPath.first(@element, "./atom:link[@rel=\"#{rel}\"]", Names::XmlNamespaces)
|
116
|
+
if a
|
117
|
+
l = a.attributes['href']
|
118
|
+
l = @base.absolutize(l, @element) if @base
|
119
|
+
end
|
120
|
+
l
|
121
|
+
end
|
122
|
+
|
123
|
+
def alt_links
|
124
|
+
REXML::XPath.match(@element, "./atom:link", Names::XmlNamespaces).select do |l|
|
125
|
+
l.attributes['rel'] == nil || l.attributes['rel'] == 'alternate'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def summarize
|
130
|
+
child_content('title')
|
131
|
+
end
|
132
|
+
|
133
|
+
# utility routine
|
134
|
+
def xpath_match(xp)
|
135
|
+
REXML::XPath.match(@element, xp, Names::XmlNamespaces)
|
136
|
+
end
|
137
|
+
|
138
|
+
# debugging
|
139
|
+
def Entry.dump(node, depth=0)
|
140
|
+
prefix = '.' * depth
|
141
|
+
name = node.getNodeName
|
142
|
+
uri = node.getNamespaceURI
|
143
|
+
if uri
|
144
|
+
puts "#{prefix} #{uri}:#{node.getNodeName}"
|
145
|
+
else
|
146
|
+
puts "#{prefix} #{node.getNodeName}"
|
147
|
+
end
|
148
|
+
Nodes.each_node(node.getChildNodes) {|child| dump(child, depth+1)}
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|