impostor 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +50 -0
  3. data/README.txt +65 -0
  4. data/Rakefile +40 -0
  5. data/lib/impostor.rb +6 -0
  6. data/lib/www/impostor/phpbb2.rb +260 -0
  7. data/lib/www/impostor/wwf79.rb +254 -0
  8. data/lib/www/impostor/wwf80.rb +264 -0
  9. data/lib/www/impostor.rb +269 -0
  10. data/test/fixtures/phpbb2-get-new_topic-form-good-response.html +725 -0
  11. data/test/fixtures/phpbb2-get-viewtopic-for-new-topic-good-response.html +364 -0
  12. data/test/fixtures/phpbb2-get-viewtopic-for-new-topic-malformed-response.html +364 -0
  13. data/test/fixtures/phpbb2-index.html +361 -0
  14. data/test/fixtures/phpbb2-logged-in.html +349 -0
  15. data/test/fixtures/phpbb2-login.html +306 -0
  16. data/test/fixtures/phpbb2-not-logged-in.html +349 -0
  17. data/test/fixtures/phpbb2-post-new_topic-good-response.html +290 -0
  18. data/test/fixtures/phpbb2-post-reply-good-response.html +290 -0
  19. data/test/fixtures/phpbb2-post-reply-throttled-response.html +290 -0
  20. data/test/fixtures/phpbb2-too-many-posts.html +290 -0
  21. data/test/fixtures/wwf79-forum_posts.html +422 -0
  22. data/test/fixtures/wwf79-general-new-topic-error.html +46 -0
  23. data/test/fixtures/wwf79-general-posting-error.html +46 -0
  24. data/test/fixtures/wwf79-good-post-forum_posts.html +462 -0
  25. data/test/fixtures/wwf79-index.html +137 -0
  26. data/test/fixtures/wwf79-logged-in.html +46 -0
  27. data/test/fixtures/wwf79-login.html +123 -0
  28. data/test/fixtures/wwf79-new-topic-forum_posts-response.html +305 -0
  29. data/test/fixtures/wwf79-new-topic-post_message_form.html +212 -0
  30. data/test/fixtures/wwf79-not-logged-in.html +128 -0
  31. data/test/fixtures/wwf79-too-many-posts.html +46 -0
  32. data/test/fixtures/wwf79-too-many-topics.html +46 -0
  33. data/test/fixtures/wwf80-general-posting-error.html +54 -0
  34. data/test/fixtures/wwf80-get-new_topic-form-good-response.html +217 -0
  35. data/test/fixtures/wwf80-get-viewtopic-for-new-topic-good-response.html +290 -0
  36. data/test/fixtures/wwf80-index.html +371 -0
  37. data/test/fixtures/wwf80-logged-in.html +52 -0
  38. data/test/fixtures/wwf80-login.html +125 -0
  39. data/test/fixtures/wwf80-new_reply_form.html +204 -0
  40. data/test/fixtures/wwf80-not-logged-in.html +136 -0
  41. data/test/fixtures/wwf80-post-new_topic-good-response.html +293 -0
  42. data/test/fixtures/wwf80-post-reply-good-response.html +331 -0
  43. data/test/fixtures/wwf80-too-many-posts.html +54 -0
  44. data/test/test_helper.rb +38 -0
  45. data/test/test_www_impostor.rb +165 -0
  46. data/test/test_www_impostor_phpbb2.rb +536 -0
  47. data/test/test_www_impostor_wwf79.rb +535 -0
  48. data/test/test_www_impostor_wwf80.rb +535 -0
  49. data/vendor/plugins/impostor/lib/autotest/discover.rb +3 -0
  50. data/vendor/plugins/impostor/lib/autotest/impostor.rb +49 -0
  51. data.tar.gz.sig +3 -0
  52. metadata +156 -0
  53. metadata.gz.sig +3 -0
@@ -0,0 +1,264 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ gem 'mechanize', '>= 0.7.0'
4
+ require 'mechanize'
5
+ require 'cgi'
6
+
7
+ ##
8
+ # Web Wiz Forums version 8.0 of the Impostor
9
+ #
10
+
11
+ class WWW::Impostor
12
+
13
+ class Wwf80 < WWW::Impostor
14
+
15
+ ##
16
+ # After initializing the parent a mechanize agent is created
17
+ #
18
+ # Additional configuration parameters:
19
+ #
20
+ # :new_reply_page
21
+ # :new_topic_page
22
+ #
23
+ # Typical configuration parameters
24
+ # { :type => :wwf80,
25
+ # :app_root => 'http://example.com/forum/',
26
+ # :login_page => 'login_user.asp',
27
+ # :new_reply_page => 'new_reply_form.asp',
28
+ # :new_topic_page => 'new_topic_form.asp',
29
+ # :user_agent => 'Windows IE 7',
30
+ # :username => 'myuser',
31
+ # :password => 'mypasswd' }
32
+
33
+ def initialize(config={})
34
+ super(config)
35
+ @agent = WWW::Mechanize.new
36
+ @agent.user_agent_alias = user_agent
37
+ # jar is a yaml file
38
+ @agent.cookie_jar.load(cookie_jar) if cookie_jar && File.exist?(cookie_jar)
39
+ @message = nil
40
+ @loggedin = false
41
+ end
42
+
43
+ ##
44
+ # clean up the state of the library and log out
45
+
46
+ def logout
47
+ return false unless @loggedin
48
+
49
+ @agent.cookie_jar.save_as(cookie_jar) if cookie_jar
50
+ save_topics
51
+
52
+ @forum = nil
53
+ @topic = nil
54
+ @message = nil
55
+
56
+ @loggedin = false
57
+ true
58
+ end
59
+
60
+ def new_topic(forum=@forum, subject=@subject, message=@message)
61
+ raise PostError.new("forum not set") unless forum
62
+ raise PostError.new("topic name not given") unless subject
63
+ raise PostError.new("message not set") unless message
64
+
65
+ login
66
+ raise PostError.new("not logged in") unless @loggedin
67
+
68
+ uri = new_topic_page
69
+ uri.query = "FID=#{forum}"
70
+
71
+ # get the submit form
72
+ begin
73
+ page = @agent.get(uri)
74
+ rescue StandardError => err
75
+ raise PostError.new(err)
76
+ end
77
+ form = page.form('frmMessageForm') rescue nil
78
+ button = form.buttons.with.name('Submit').first rescue nil
79
+ raise PostError.new("post form not found") unless button && form
80
+
81
+ # set up the form and submit it
82
+ form.subject = subject
83
+ form.message = message
84
+ begin
85
+ page = @agent.submit(form, button)
86
+ rescue StandardError => err
87
+ raise PostError.new(err)
88
+ end
89
+
90
+ error = page.search("//table[@class='errorTable']")
91
+ if error
92
+ msgs = error.search("//td")
93
+
94
+ # throttled
95
+ too_many = (msgs.last.innerText =~
96
+ /You have exceeded the number of posts permitted in the time span/ rescue
97
+ false)
98
+ raise ThrottledError.new(msgs.last.innerText.gsub(/\s+/m,' ').strip) if too_many
99
+
100
+ # general error
101
+ had_error = (error.last.innerText =~
102
+ /Error: Message Not Posted/ rescue
103
+ false)
104
+ raise PostError.new(error.last.innerText.gsub(/\s+/m,' ').strip) if had_error
105
+ end
106
+
107
+ # look up the new topic id
108
+ form = page.form('frmMessageForm') rescue nil
109
+ topic = form['TID'].to_i rescue 0
110
+ raise PostError.new('unexpected new topic ID') if topic < 1
111
+
112
+ # save new topic id and topic name
113
+ add_subject(forum, topic, subject)
114
+
115
+ @forum=forum; @topic=topic; @subject=subject; @message=message
116
+ return true
117
+ end
118
+
119
+ ##
120
+ # Attempt to post to the forum
121
+
122
+ def post(forum = @forum, topic = @topic, message = @message)
123
+ raise PostError.new("forum not set") unless forum
124
+ raise PostError.new("topic not set") unless topic
125
+ raise PostError.new("message not set") unless message
126
+
127
+ login
128
+ raise PostError.new("not logged in") unless @loggedin
129
+
130
+ uri = new_reply_page
131
+ uri.query = "TID=#{topic}"
132
+
133
+ # get the submit form
134
+ begin
135
+ page = @agent.get(uri)
136
+ rescue StandardError => err
137
+ raise PostError.new(err)
138
+ end
139
+
140
+ form = page.form('frmMessageForm') rescue nil
141
+ button = form.buttons.with.name('Submit').first rescue nil
142
+ raise PostError.new("post form not found") unless button && form
143
+
144
+ # set up the form and submit it
145
+ form.message = message
146
+ begin
147
+ page = @agent.submit(form, button)
148
+ rescue StandardError => err
149
+ raise PostError.new(err)
150
+ end
151
+
152
+ error = page.search("//table[@class='errorTable']")
153
+ if error
154
+ msgs = error.search("//td")
155
+
156
+ # throttled
157
+ too_many = (msgs.last.innerText =~
158
+ /You have exceeded the number of posts permitted in the time span/ rescue
159
+ false)
160
+ raise ThrottledError.new(msgs.last.innerText.gsub(/\s+/m,' ').strip) if too_many
161
+
162
+ # general error
163
+ had_error = (error.last.innerText =~
164
+ /Error: Message Not Posted/ rescue
165
+ false)
166
+ raise PostError.new(error.last.innerText.gsub(/\s+/m,' ').strip) if had_error
167
+ end
168
+
169
+ @forum=forum; @topic=topic; @subject=get_subject(forum,topic); @message=message
170
+ return true
171
+ end
172
+
173
+ ##
174
+ # Get the new reply page for the application (specific to WWF8.0)
175
+
176
+ def new_reply_page
177
+ URI.join(app_root, config[:new_reply_page])
178
+ end
179
+
180
+ ##
181
+ # Get the new topic page for the application (specific to WWF8.0)
182
+
183
+ def new_topic_page
184
+ URI.join(app_root, config[:new_topic_page])
185
+ end
186
+
187
+ ##
188
+ # does the work of logging into WWF 8.0
189
+
190
+ def login
191
+ return true if @loggedin
192
+
193
+ # get the login page
194
+ page = fetch_login_page
195
+
196
+ # return if we are already logged in from a cookie state
197
+ return true if logged_in?(page)
198
+
199
+ # setup the form and submit
200
+ form, button = login_form_and_button(page)
201
+ page = post_login(form, button)
202
+
203
+ # set up the rest of the state if we are logged in
204
+ @loggedin = logged_in?(page)
205
+ load_topics if @loggedin
206
+
207
+ @loggedin
208
+ end
209
+
210
+ def version
211
+ @version ||= self.class.to_s
212
+ end
213
+
214
+ protected
215
+
216
+ ##
217
+ # does the work of posting the login form
218
+
219
+ def post_login(form, button)
220
+ begin
221
+ page = @agent.submit(form, button)
222
+ rescue StandardError => err
223
+ raise LoginError.new(err)
224
+ end
225
+ end
226
+
227
+ ##
228
+ # returns the login form and its button from the login page
229
+
230
+ def login_form_and_button(page)
231
+ form = page.forms.with.name('frmLogin').first rescue nil
232
+ raise LoginError.new("unknown login page format") unless form
233
+
234
+ button = form.buttons.with.name('Submit').first
235
+ form['name'] = username
236
+ form['password'] = password
237
+
238
+ return form, button
239
+ end
240
+
241
+ ##
242
+ # fetches the login page
243
+
244
+ def fetch_login_page
245
+ begin
246
+ page = @agent.get(login_page)
247
+ rescue StandardError => err
248
+ raise LoginError.new(err)
249
+ end
250
+ end
251
+
252
+ ##
253
+ # Checks if the agent is already logged by stored cookie
254
+
255
+ def logged_in?(page)
256
+ mm = page.search("//a[@class='nav']")
257
+ return false unless mm
258
+ mm.each do |m|
259
+ return true if (m.innerText =~ /Logout \[#{username}\]/ rescue false)
260
+ end
261
+ false
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,269 @@
1
+ require 'rubygems'
2
+ Dir.glob(File.join(File.dirname(__FILE__), 'impostor/*.rb')).each {|f| require f }
3
+
4
+ module WWW
5
+
6
+ ##
7
+ # imPOSTor posts messages to non-RESTful forums and blogs
8
+ #
9
+ # == Example
10
+ # require 'rubygems'
11
+ # require 'impostor'
12
+ #
13
+ # # config yaml has options specefic to wwf79, wwf80, phpbb2, etc.
14
+ # # read the impostor docs for options to the kind of forum in use
15
+ # # config can be keyed by symbols or strings
16
+ # post = WWW::Impostor.new(YAML.load_file('config.yml'))
17
+ # message = %q!hello world is to application
18
+ # programmers as tea pots are to graphics programmers!
19
+ # # your application store forum and topic ids
20
+ # post.post(forum=5,topic=10,message)
21
+ # # make a new topic
22
+ # subject = "about programmers..."
23
+ # post.new_topic(forum=7,subject,message)
24
+ # post.logout
25
+ #
26
+ # keys and values that can be set in the impostor configuration
27
+ #
28
+ # :type - kind of imPOSTor, :phpbb2, :wwf79, :wwf80, etc.
29
+ # :username - forum username
30
+ # :password - forum password
31
+ # :topics_cache - cache of forum topics
32
+ # :user_agent - Mechanize browser user-agent
33
+ # :cookie_jar - saved cookies from Mechanize browser
34
+ # :app_root - url to forum
35
+ # :login_page - forum login page
36
+ #
37
+ # See documentation for each type of imPOSTor for additional configuration
38
+ # parameters that are needed for the specific kind of imPOSTor. A sample
39
+ # configuration is provided in the documentation for each.
40
+
41
+ class Impostor
42
+
43
+ class << self #:nodoc:
44
+ alias orig_new new
45
+ def new(conf)
46
+ klass = WWW::Impostor.create(conf)
47
+ klass.orig_new(conf)
48
+ end
49
+ end
50
+
51
+ ##
52
+ # Gem version of Impostor
53
+
54
+ VERSION = '0.0.1'
55
+
56
+ ##
57
+ # An application error
58
+
59
+ class ImpostorError < RuntimeError
60
+
61
+ ##
62
+ # The original exception
63
+
64
+ attr_accessor :original_exception
65
+
66
+ ##
67
+ # Creates a new ImpostorError with +message+ and +original_exception+
68
+
69
+ def initialize(e)
70
+ exception = e.class == String ? StandardError.new(e) : e
71
+ @original_exception = exception
72
+ message = "Impostor error: #{exception.message} (#{exception.class})"
73
+ super message
74
+ end
75
+
76
+ end
77
+
78
+ ##
79
+ # An error for impostor login failure
80
+
81
+ class LoginError < ImpostorError; end
82
+
83
+ ##
84
+ # An error for impostor post failure
85
+
86
+ class PostError < ImpostorError; end
87
+
88
+ ##
89
+ # An error for impostor when a topic id can't be found based on a
90
+ # name/title.
91
+
92
+ class TopicError < ImpostorError; end
93
+
94
+ ##
95
+ # An error for impostor when the receiving forum rejects the post due to
96
+ # a throttling or spam error but which the user can re-attempt at a later
97
+ # time.
98
+
99
+ class ThrottledError < ImpostorError; end
100
+
101
+ ##
102
+ # Pass in a config hash to initialize
103
+
104
+ def initialize(conf={})
105
+ @config = conf
106
+ load_topics
107
+ end
108
+
109
+ ##
110
+ # Instantiate a specific impostor based on its symbol name
111
+
112
+ def self.create(conf={})
113
+ type = conf[:type] || conf[:type.to_s]
114
+ clz = type.is_a?(Class) ? type : eval("WWW::Impostor::#{type.to_s.capitalize}")
115
+ clz
116
+ end
117
+
118
+ ##
119
+ # Access the current config and key it without regard for symbols or strings
120
+
121
+ def config(*key)
122
+ return @config if key.empty?
123
+ @config[key.first.to_sym] || @config[key.first.to_s]
124
+ end
125
+
126
+ ##
127
+ # Get/set the application version that impostor is interfacing with
128
+
129
+ attr_accessor :version
130
+
131
+ ##
132
+ # Login to the forum, returns true if logged in, false otherwise
133
+
134
+ def login; end
135
+
136
+ ##
137
+ # Log out of the forum, true if logged in, false otherwise
138
+
139
+ def logout; end
140
+
141
+ ##
142
+ # Load the topics that the impostor already knows about
143
+
144
+ def load_topics
145
+ cache = config[:topics_cache] ||= ""
146
+ if File::exist?(cache)
147
+ @topics = YAML::load_file(cache)
148
+ else
149
+ @topics = Hash.new
150
+ end
151
+ end
152
+
153
+ ##
154
+ # Add subject to topics hash
155
+
156
+ def add_subject(forum,topic,name)
157
+ if @topics[forum].nil?
158
+ @topics[forum] = {topic, name}
159
+ else
160
+ @topics[forum][topic] = name
161
+ end
162
+ end
163
+
164
+ ##
165
+ # Save the topics
166
+
167
+ def save_topics
168
+ cache = config[:topics_cache] ||= ""
169
+ if File::exist?(cache)
170
+ File.open(cache, 'w') do |out|
171
+ YAML.dump(@topics, out)
172
+ end
173
+ end
174
+ end
175
+
176
+ ##
177
+ # Post the message
178
+
179
+ def post(forum = @forum, topic = @topic, message = @message); end
180
+
181
+ ##
182
+ # get/set the current message
183
+
184
+ attr_accessor :message
185
+
186
+ ##
187
+ # get/set the current subject
188
+
189
+ attr_accessor :subject
190
+
191
+ ##
192
+ # Get/set the form id
193
+
194
+ attr_accessor :forum
195
+
196
+ ##
197
+ # Get/set the topic id
198
+
199
+ attr_accessor :topic
200
+
201
+ ##
202
+ # Get the topic name (subject) based on forum and topic ids
203
+
204
+ def get_subject(forum = @forum, topic = @topic)
205
+ if @topics && @topics[forum]
206
+ return @topics[forum][topic]
207
+ end
208
+ nil
209
+ end
210
+
211
+ ##
212
+ # Make a new topic
213
+
214
+ def new_topic(forum=@forum, subject=@subject, message=@message); end
215
+
216
+ ##
217
+ # Gets the application root of the application such as
218
+ # http://example.com/phpbb or http://example.com/forums
219
+
220
+ def app_root
221
+ config[:app_root]
222
+ end
223
+
224
+ protected
225
+
226
+ ##
227
+ # Get the topics cache
228
+
229
+ def topics_cache
230
+ config[:topics_cache]
231
+ end
232
+
233
+ ##
234
+ # Get the login page for the application
235
+
236
+ def login_page
237
+ URI.join(app_root, config[:login_page])
238
+ end
239
+
240
+ ##
241
+ # Get the username for the application
242
+
243
+ def username
244
+ config[:username]
245
+ end
246
+
247
+ ##
248
+ # Get the password for the application
249
+
250
+ def password
251
+ config[:password]
252
+ end
253
+
254
+ ##
255
+ # A Mechanize user agent name, see the mechanize documentation
256
+ # 'Linux Mozilla', 'Mac Safari', 'Windows IE 7', etc.
257
+
258
+ def user_agent
259
+ config[:user_agent]
260
+ end
261
+
262
+ ##
263
+ # is a yaml file for WWW::Mechanize::CookieJar
264
+
265
+ def cookie_jar
266
+ config[:cookie_jar]
267
+ end
268
+ end
269
+ end