ape 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/CHANGELOG +1 -0
  2. data/LICENSE +19 -0
  3. data/Manifest +45 -0
  4. data/README +12 -0
  5. data/ape.gemspec +57 -0
  6. data/bin/ape_server +28 -0
  7. data/lib/ape.rb +982 -0
  8. data/lib/ape/atomURI.rb +73 -0
  9. data/lib/ape/auth/google_login_credentials.rb +96 -0
  10. data/lib/ape/auth/wsse_credentials.rb +25 -0
  11. data/lib/ape/authent.rb +42 -0
  12. data/lib/ape/categories.rb +95 -0
  13. data/lib/ape/collection.rb +51 -0
  14. data/lib/ape/crumbs.rb +39 -0
  15. data/lib/ape/entry.rb +151 -0
  16. data/lib/ape/escaper.rb +29 -0
  17. data/lib/ape/feed.rb +117 -0
  18. data/lib/ape/handler.rb +34 -0
  19. data/lib/ape/html.rb +17 -0
  20. data/lib/ape/invoker.rb +54 -0
  21. data/lib/ape/invokers/deleter.rb +31 -0
  22. data/lib/ape/invokers/getter.rb +80 -0
  23. data/lib/ape/invokers/poster.rb +57 -0
  24. data/lib/ape/invokers/putter.rb +46 -0
  25. data/lib/ape/layout/ape.css +56 -0
  26. data/lib/ape/layout/ape_logo.png +0 -0
  27. data/lib/ape/layout/index.html +54 -0
  28. data/lib/ape/layout/info.png +0 -0
  29. data/lib/ape/names.rb +24 -0
  30. data/lib/ape/print_writer.rb +21 -0
  31. data/lib/ape/samples.rb +180 -0
  32. data/lib/ape/samples/atom_schema.txt +338 -0
  33. data/lib/ape/samples/basic_entry.eruby +16 -0
  34. data/lib/ape/samples/categories_schema.txt +69 -0
  35. data/lib/ape/samples/mini_entry.eruby +8 -0
  36. data/lib/ape/samples/service_schema.txt +187 -0
  37. data/lib/ape/samples/unclean_xhtml_entry.eruby +21 -0
  38. data/lib/ape/server.rb +32 -0
  39. data/lib/ape/service.rb +12 -0
  40. data/lib/ape/validator.rb +65 -0
  41. data/lib/ape/version.rb +9 -0
  42. data/scripts/go.rb +29 -0
  43. data/test/test_helper.rb +17 -0
  44. data/test/unit/authent_test.rb +35 -0
  45. data/test/unit/invoker_test.rb +25 -0
  46. data/test/unit/samples_test.rb +36 -0
  47. metadata +111 -0
@@ -0,0 +1,29 @@
1
+ # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
+ # Use is subject to license terms - see file "LICENSE"
3
+ module Ape
4
+ class Escaper
5
+ def Escaper.escape(text)
6
+ text.gsub(/([&<'">])/) do
7
+ case $1
8
+ when '&' then '&amp;'
9
+ when '<' then '&lt;'
10
+ when "'" then '&apos;'
11
+ when '"' then '&quot;'
12
+ when '>' then '&gt;'
13
+ end
14
+ end
15
+ end
16
+
17
+ def Escaper.unescape(text)
18
+ text.gsub(/&([^;]*);/) do
19
+ case $1
20
+ when 'lt' then '<'
21
+ when 'amp' then '&'
22
+ when 'gt' then '>'
23
+ when 'apos' then "'"
24
+ when 'quot' then '"'
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,117 @@
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 Feed
9
+ # Load up a collection feed from its URI. Return an array of <entry> objects.
10
+ # follow <link rel="next" /> pointers as required to get the whole
11
+ # collection
12
+ def Feed.read(uri, name, ape, report=true)
13
+
14
+ entries = []
15
+ uris = []
16
+ next_page = uri
17
+ page_num = 1
18
+
19
+ while (next_page) && (page_num < 10) do
20
+
21
+ label = "Page #{page_num} of #{name}"
22
+ uris << next_page
23
+ page = ape.check_resource(next_page, label, Names::AtomMediaType, report)
24
+ break unless page
25
+
26
+ # * Validate it
27
+ Validator.validate(Samples.atom_RNC, page.body, label, ape) if report
28
+
29
+ # XML-parse the feed
30
+ error = "not well-formed"
31
+ feed = nil
32
+ begin
33
+ feed = REXML::Document.new(page.body, { :raw => nil })
34
+ rescue Exception
35
+ error = $!.to_s
36
+ feed = nil
37
+ end
38
+ if feed == nil
39
+ ape.error "Can't parse #{label} at #{next_page}, Parser said: #{$!}" if report
40
+ break
41
+ end
42
+
43
+ feed = feed.root
44
+ if feed == nil
45
+ ape.warning "#{label} is empty."
46
+ break
47
+ end
48
+
49
+ page_entries = REXML::XPath.match(feed, "./atom:entry", Names::XmlNamespaces)
50
+ if page_entries.empty? && report
51
+ ape.info "#{label} has no entries."
52
+ end
53
+
54
+ entries += page_entries.map { |e| Entry.new(e, next_page)}
55
+
56
+ next_link = REXML::XPath.first(feed, "./atom:link[@rel=\"next\"]", Names::XmlNamespaces)
57
+ if next_link
58
+ next_link = next_link.attributes['href']
59
+ base = AtomURI.new(next_page)
60
+ next_link = base.absolutize(next_link, feed)
61
+ if uris.index(next_link)
62
+ ape.error "Collection contains circular 'next' linkage: #{next_link}" if report
63
+ break
64
+ end
65
+ page_num += 1
66
+ end
67
+ next_page = next_link
68
+ end
69
+
70
+ if report && next_page
71
+ ape.warning "Stopped reading collection after #{page_num} pages."
72
+ end
73
+
74
+ # all done unless we're error-checking
75
+ return entries unless report
76
+
77
+ # Ensure that entries are ordered by app:edited
78
+ last_date = nil
79
+ with_app_date = 0
80
+ clean = true
81
+ entries.each do |e|
82
+ datestr = e.child_content("edited", Names::AppNamespace)
83
+ error = nil
84
+ if datestr
85
+ if datestr =~ /\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|([-+]\d\d:\d\d))/
86
+ begin
87
+ date = Time.parse(datestr)
88
+ with_app_date += 1
89
+ if last_date && (date > last_date)
90
+ error = "app:edited values out of order, d #{date} ld #{last_date}"
91
+ end
92
+ last_date = date
93
+ rescue ArgumentError
94
+ error = "invalid app:edited value: #{datestr}"
95
+ end
96
+ else
97
+ error = "invalid app:edited child: #{datestr}"
98
+ end
99
+ if error
100
+ title = e.child_content "title"
101
+ ape.error "In entry with title '#{title}', #{error}."
102
+ clean = false
103
+ end
104
+ end
105
+ end
106
+ if with_app_date < entries.size
107
+ ape.error "#{entries.size - with_app_date} of #{entries.size} entries in #{name} lack app:edited elements."
108
+ clean = false
109
+ end
110
+
111
+ ape.good "#{name} has correct app:edited value order." if clean
112
+
113
+ entries
114
+ end
115
+ end
116
+ end
117
+
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'mongrel'
3
+ require 'ape'
4
+
5
+ module Ape
6
+ class Handler < Mongrel::HttpHandler
7
+ def process(request, response)
8
+ cgi = Mongrel::CGIWrapper.new(request, response)
9
+
10
+ uri = cgi['uri'].strip
11
+ user = cgi['username'].strip
12
+ pass = cgi['password'].strip
13
+
14
+ # invoke_ape uri, user, pass, request, response
15
+
16
+ if uri.empty?
17
+ response.start(200, true) do |header, body|
18
+ header['Content-Type'] = 'text/plain'
19
+ body << 'URI argument is required'
20
+ end
21
+ return
22
+ end
23
+
24
+ format = request.params['HTTP_ACCEPT'] == 'text/plain' ? 'text' : 'html'
25
+ ape = Ape.new({ :crumbs => true, :output => format })
26
+ (user && pass) ? ape.check(uri, user, pass) : ape.check(uri)
27
+
28
+ response.start(200, true) do |header, body|
29
+ header['Content-Type'] = 'text/html'
30
+ ape.report(body)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
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 HTML
6
+ def HTML.error(message, output=STDOUT)
7
+ headers(output)
8
+ output.puts <<EndOfText
9
+ <title>Error: #{message}</title>
10
+ </head>
11
+ <body>
12
+ <h2>Error</h2>
13
+ <p>#{message}.</p>
14
+ EndOfText
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,54 @@
1
+ # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
+ # Use is subject to license terms - see file "LICENSE"
3
+ require 'net/https'
4
+
5
+ module Ape
6
+ class Invoker
7
+ attr_reader :last_error, :crumbs, :response
8
+
9
+ def initialize(uriString, authent)
10
+ @last_error = nil
11
+ @crumbs = Crumbs.new
12
+ @uri = AtomURI.check(uriString)
13
+ if (@uri.class == String)
14
+ @last_error = @uri
15
+ end
16
+ @authent = authent
17
+ @authent_checker = 0
18
+ end
19
+
20
+ def header(which)
21
+ @response[which]
22
+ end
23
+
24
+ def prepare_http
25
+ http = Net::HTTP.new(@uri.host, @uri.port)
26
+ if @uri.scheme == 'https'
27
+ http.use_ssl = true
28
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
29
+ end
30
+ http.set_debug_output @crumbs if @crumbs
31
+ http
32
+ end
33
+
34
+ def need_authentication?(req)
35
+ if @response.instance_of?(Net::HTTPUnauthorized) && @authent
36
+ #tries to authenticate just two times in order to avoid infinite loops
37
+ raise AuthenticationError, 'Authentication is required' unless @authent_checker <= 1
38
+ @authent_checker += 1
39
+
40
+ @authent.add_to req, header('WWW-Authenticate')
41
+ #clean the request body attribute, if we don't do it http.request(req, body) will raise an exception
42
+ req.body = nil unless req.body.nil?
43
+ return true
44
+ end
45
+ return false
46
+ end
47
+
48
+ def restart_authent_checker
49
+ @authent_checker = 0
50
+ end
51
+ end
52
+ end
53
+
54
+ Dir[File.dirname(__FILE__) + '/invokers/*.rb'].each { |l| require l }
@@ -0,0 +1,31 @@
1
+ # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
+ # Use is subject to license terms - see file "LICENSE"
3
+ require 'net/http'
4
+
5
+ module Ape
6
+ class Deleter < Invoker
7
+
8
+ def delete( req = nil )
9
+ req = Net::HTTP::Delete.new(AtomURI.on_the_wire(@uri)) unless req
10
+
11
+ begin
12
+ http = prepare_http
13
+
14
+ http.start do |connection|
15
+ @response = connection.request(req)
16
+
17
+ return delete(req) if need_authentication?(req)
18
+ restart_authent_checker
19
+
20
+ return true if @response.kind_of? Net::HTTPSuccess
21
+
22
+ @last_error = @response.message
23
+ return false
24
+ end
25
+ rescue Exception
26
+ @last_error = "Can't connect to #{@uri.host} on port #{@uri.port}: #{$!}"
27
+ return nil
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,80 @@
1
+ # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
+ # Use is subject to license terms - see file "LICENSE"
3
+ require 'net/http'
4
+
5
+ module Ape
6
+ class Getter < Invoker
7
+ attr_reader :contentType, :charset, :body, :security_warning
8
+
9
+ def get(contentType = nil, depth = 0, req = nil)
10
+ req = Net::HTTP::Get.new(AtomURI.on_the_wire(@uri)) unless req
11
+ @last_error = nil
12
+
13
+ return false if document_failed?(depth, req)
14
+
15
+ begin
16
+ http = prepare_http
17
+
18
+ http.start do |connection|
19
+ @response = connection.request(req)
20
+
21
+ if need_authentication?(req)
22
+ @security_warning = true unless http.use_ssl?
23
+ return get(contentType, depth + 1, req)
24
+ end
25
+ restart_authent_checker
26
+
27
+ case @response
28
+ when Net::HTTPSuccess
29
+ return getBody(contentType)
30
+
31
+ when Net::HTTPRedirection
32
+ redirect_to = @uri.merge(@response['location'])
33
+ @uri = AtomURI.check(redirect_to)
34
+ return get(contentType, depth + 1)
35
+
36
+ else
37
+ @last_error = @response.message
38
+ return false
39
+ end
40
+ end
41
+ rescue Exception
42
+ @last_error = "Can't connect to #{@uri.host} on port #{@uri.port}: #{$!}"
43
+ return false
44
+ end
45
+ end
46
+
47
+ def document_failed?(depth, req)
48
+ if (depth > 10)
49
+ if need_authentication?(req)
50
+ #Authentication required
51
+ @last_error = "Authentication is required"
52
+ else
53
+ # too many redirects
54
+ @last_error = "Too many redirects"
55
+ end
56
+ return true
57
+ end
58
+ return false
59
+ end
60
+
61
+ def getBody contentType
62
+
63
+ if contentType
64
+ @contentType = @response['Content-Type']
65
+ # XXX TODO - better regex
66
+ if @contentType =~ /^([^;]*);/
67
+ @contentType = $1
68
+ end
69
+
70
+ if contentType != @contentType
71
+ @last_error = "Content-type must be '#{contentType}', not '#{@contentType}'"
72
+ end
73
+ end
74
+
75
+ @body = @response.body
76
+ return true
77
+ end
78
+ end
79
+ end
80
+
@@ -0,0 +1,57 @@
1
+ # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
+ # Use is subject to license terms - see file "LICENSE"
3
+ require 'net/http'
4
+
5
+ module Ape
6
+ class Poster < Invoker
7
+ attr_reader :entry, :uri
8
+
9
+ def initialize(uriString, authent)
10
+ super uriString, authent
11
+ @headers = {}
12
+ @entry = nil
13
+ end
14
+
15
+ def set_header(name, val)
16
+ @headers[name] = val
17
+ end
18
+
19
+ def post(contentType, body, req = nil)
20
+ req = Net::HTTP::Post.new(AtomURI.on_the_wire(@uri)) if req.nil?
21
+ req.set_content_type contentType
22
+ @headers.each { |k, v| req[k]= v }
23
+
24
+ begin
25
+ http = prepare_http
26
+
27
+ http.start do |connection|
28
+ @response = connection.request(req, body)
29
+
30
+ return post(contentType, body, req) if need_authentication?(req)
31
+ restart_authent_checker
32
+
33
+ if @response.code != '201'
34
+ @last_error = @response.message
35
+ return false
36
+ end
37
+
38
+ if (!((@response['Content-type'] =~ %r{^application/atom\+xml}) ||
39
+ (@response['Content-type'] =~ %r{^application/atom\+xml;type=entry})))
40
+ return true
41
+ end
42
+
43
+ begin
44
+ @entry = Entry.new(@response.body)
45
+ return true
46
+ rescue ArgumentError
47
+ @last_error = @entry.broken
48
+ return false
49
+ end
50
+ end
51
+ rescue Exception
52
+ @last_error = "Can't connect to #{@uri.host} on port #{@uri.port}: #{$!}"
53
+ return false
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,46 @@
1
+ # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
+ # Use is subject to license terms - see file "LICENSE"
3
+ require 'net/http'
4
+
5
+ module Ape
6
+ class Putter < Invoker
7
+ attr_reader :headers
8
+
9
+ def initialize(uriString, authent)
10
+ super uriString, authent
11
+ @headers = {}
12
+ end
13
+
14
+ def set_header(name, val)
15
+ @headers[name] = val
16
+ end
17
+
18
+ def put(contentType, body, req = nil)
19
+ req = Net::HTTP::Put.new(AtomURI.on_the_wire(@uri)) unless req
20
+
21
+ req.set_content_type contentType
22
+ @headers.each { |k, v| req[k]= v }
23
+
24
+ begin
25
+ http = prepare_http
26
+
27
+ http.start do |connection|
28
+ @response = connection.request(req, body)
29
+
30
+ return put(contentType, body, req) if need_authentication?(req)
31
+ restart_authent_checker
32
+
33
+ unless @response.kind_of? Net::HTTPSuccess
34
+ @last_error = @response.message
35
+ return false
36
+ end
37
+
38
+ return true
39
+ end
40
+ rescue Exception
41
+ @last_error = "Can't connect to #{@uri.host} on port #{@uri.port}: #{$!}"
42
+ return nil
43
+ end
44
+ end
45
+ end
46
+ end