ape 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|