me_sd 0.0.3beta

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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/me_sd.rb +384 -0
  3. metadata +59 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 941dc0362e5bc77e988b58c63f85819035a7b4a1
4
+ data.tar.gz: d67a49a6fc43bd400f51a08770a3c613e69f049c
5
+ SHA512:
6
+ metadata.gz: 5fe673eefd73e36425110010575f8e4fccc89a6de38c045843e24d2439b69ba54ffcf48fb97a9e26eb932a222e42df98d83131ed81fa81123f66f41de735bb7b
7
+ data.tar.gz: 85f5debddf474d137ee6e29574a23a0c943ddca418f4ce70b99ad0d96e12a58fb1fcded640e40610953012debe34b2995e4c61f57ad77322565e515582a55661
data/lib/me_sd.rb ADDED
@@ -0,0 +1,384 @@
1
+ # sd = MESD.new({ host: "192.168.0.150", port: "8080", username: "user", password: "P@ssw0rd" })
2
+ # # default port is "80"
3
+ # => true
4
+ # unless sd.errors
5
+ # requests = sd.get_all_requests
6
+ # requests[0].data
7
+ # end
8
+ # => #<MESD::Request:0x0000000265d360 @id="29", ..., @description="request decription", @resolution="request resolution", ...>
9
+ # request = Request.new({ session: sd.session, id: 29 })
10
+ # request.data(:name, :resolution)
11
+ # => #<MESD::Request:0x000000023b6800 @id="29", ..., @name="request name", @resolution="request resolution">
12
+ # request.get_resolution
13
+ # => "request resolution"
14
+
15
+ class MESD
16
+ attr_accessor :session, :last_error, :curobj, :current_body
17
+
18
+ require "net/http"
19
+ EXCEPTIONS = [Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::EHOSTUNREACH, EOFError,
20
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError]
21
+
22
+ def initialize(args)
23
+ host = args[:host]
24
+ port = args[:port] || "80"
25
+ username = args[:username]
26
+ password = args[:password]
27
+ uri = URI("http://#{host}:#{port}")
28
+ begin
29
+ Net::HTTP.start(uri.host, uri.port) do |http|
30
+ request = http.get(uri)
31
+ cookie = request.response["set-cookie"]
32
+ uri = "#{uri}/j_security_check"
33
+ auth_data = ""\
34
+ "j_username=#{username}&"\
35
+ "j_password=#{password}&"\
36
+ "AdEnable=false&"\
37
+ "DomainCount=0&"\
38
+ "LDAPEnable=false&"\
39
+ "LocalAuth=No&"\
40
+ "LocalAuthWithDomain=No&"\
41
+ "dynamicUserAddition_status=true&"\
42
+ "hidden=Select+a+Domain&"\
43
+ "hidden=For+Domain&"\
44
+ "localAuthEnable=true&"\
45
+ "loginButton=Login&"\
46
+ "logonDomainName=-1&"\
47
+ ""
48
+ auth_headers = {
49
+ "Referer" => "http://#{host}:#{port}",
50
+ "Host" => "#{host}:#{port}",
51
+ "Cookie" => "#{cookie};",
52
+ }
53
+ request = http.post(uri, auth_data, auth_headers)
54
+ @session = {
55
+ host: host,
56
+ port: port,
57
+ cookie: cookie,
58
+ }
59
+ @last_error = "wrong credentials" unless self.session_healthy?(self.session)
60
+ end
61
+ rescue *EXCEPTIONS => @last_error
62
+ end
63
+ end
64
+
65
+ # logs in and tries to find out is session healthy
66
+ # criteria: logout button is present
67
+ def session_healthy?(session)
68
+ return false unless session
69
+ session_healthy = false
70
+ uri = URI("http://#{session[:host]}:#{session[:port]}/MySchedule.do")
71
+ begin
72
+ Net::HTTP.start(uri.host, uri.port) do |http|
73
+ request = Net::HTTP::Get.new(uri)
74
+ request.add_field("Cookie", "#{session[:cookie]}")
75
+ request = http.request(request)
76
+ # ...
77
+ # <a style="display:inline" href="\&quot;javascript:" prelogout('null')\"="">Log out</a>
78
+ # ...
79
+ session_healthy = true if /preLogout/.match(request.body)
80
+ end
81
+ rescue *EXCEPTIONS => @last_error
82
+ end
83
+ session_healthy
84
+ end
85
+
86
+ def get_all_requests
87
+ requests = Array.new
88
+ select_all_requests
89
+ puts "Getting total #{@curobj['_TL']} requests:"
90
+ get_requests_urls(@current_body).each { |url| requests.push(Request.new({ session: @session, url: url })) }
91
+ begin
92
+ not_last_page = next_page
93
+ get_requests_urls(@current_body).each { |url| requests.push(Request.new({ session: @session, url: url })) }
94
+ end while not_last_page
95
+ requests
96
+ end
97
+
98
+ def select_all_requests
99
+ session = self.session
100
+ return false unless session
101
+ uri = URI("http://#{session[:host]}:#{session[:port]}/WOListView.do")
102
+ begin
103
+ Net::HTTP.start(uri.host, uri.port) do |http|
104
+ data = "globalViewName=All_Requests&viewName=All_Requests"
105
+ headers = {
106
+ "Referer" => "http://#{session[:host]}:#{session[:port]}/WOListView.do",
107
+ "Host" => "#{session[:host]}:#{session[:port]}",
108
+ "Cookie" => "#{session[:cookie]}",
109
+ }
110
+ request = http.post(uri, data, headers)
111
+ @current_body = request.response.body
112
+ @curobj = get_curobj
113
+ end
114
+ rescue *EXCEPTIONS => @last_error
115
+ end
116
+ end
117
+
118
+ def next_page
119
+ require "date"
120
+ session = self.session
121
+ return false unless session
122
+ # 13 digits time
123
+ timestamp = DateTime.now.strftime("%Q")
124
+ uri = URI("http://#{session[:host]}:#{session[:port]}/STATE_ID/#{timestamp}/"\
125
+ "RequestsView.cc?UNIQUE_ID=RequestsView&SUBREQUEST=true")
126
+ begin
127
+ Net::HTTP.start(uri.host, uri.port) do |http|
128
+ request = Net::HTTP::Get.new(uri)
129
+ request.add_field("Referer", "http://#{session[:host]}:#{session[:port]}/WOListView.do")
130
+ @curobj = get_curobj
131
+ return false unless @curobj
132
+ print "#{(@curobj["_FI"].to_f / @curobj["_TL"].to_f * 100).round}%.."
133
+ # increment page number
134
+ @curobj["_PN"] = (@curobj["_PN"].to_i + 1).to_s
135
+ # update first item
136
+ @curobj["_FI"] = (@curobj["_FI"].to_i + @curobj["_PL"].to_i).to_s
137
+ # @curobj.flatten.join("/") =>
138
+ # "_PN/2/_PL/25/_TL/28/globalViewName/All_Requests/_TI/25/_FI/1/_SO/D/viewName/All_Requests"
139
+ request.add_field("Cookie",
140
+ "#{session[:cookie]}; "\
141
+ "STATE_COOKIE=%26RequestsView/ID/#{@curobj['ID']}/VGT/#{timestamp}/#{@curobj.flatten.join('/')}"\
142
+ "/_VMD/1/ORIGROOT/#{@curobj['ID']}%26_REQS/_RVID/RequestsView/_TIME/#{timestamp}; "\
143
+ "301RequestsshowThreadedReq=showThreadedReqshow; "\
144
+ "301RequestshideThreadedReq=hideThreadedReqhide"\
145
+ ""
146
+ )
147
+ request = http.request(request)
148
+ @current_body = request.response.body
149
+ end
150
+ rescue *EXCEPTIONS => @last_error
151
+ end
152
+ # if (first item + per page) > total items then it is the last page
153
+ if (@curobj["_FI"].to_i + @curobj["_PL"].to_i) > @curobj["_TL"].to_i
154
+ puts "100%"
155
+ return false
156
+ end
157
+ true
158
+ end
159
+
160
+ def get_curobj
161
+ body = @current_body
162
+ # somewhere in body
163
+ # "<Script>curObj=V33;curObj[\"_PN\"]=\"1\";curObj[\"_PL\"]=\"25\";curObj[\"_TL\"]=\"28\";"\
164
+ # "curObj[\"globalViewName\"]=\"All_Requests\";curObj[\"_TI\"]=\"25\";curObj[\"_FI\"]=\"1\";"\
165
+ # "curObj[\"_SO\"]=\"D\";curObj[\"viewName\"]=\"All_Requests\";</Script>"
166
+ search_start_str = "<Script>curObj=V"
167
+ curobj_start_pos = body.index(search_start_str)
168
+ return false unless curobj_start_pos
169
+ v = /<Script>curObj=V(?<V>\d+);/.match(body)["V"]
170
+ curobj_end_pos = body.index("</Script>", curobj_start_pos)
171
+ curobj_raw = body[curobj_start_pos + search_start_str.size + v.size...curobj_end_pos]
172
+ # curobj_raw =>
173
+ # "curObj[\"_PN\"]=\"1\";curObj[\"_PL\"]=\"25\";curObj[\"_TL\"]=\"28\";"\
174
+ # "curObj[\"globalViewName\"]=\"All_Requests\";curObj[\"_TI\"]=\"25\";curObj[\"_FI\"]=\"1\";"\
175
+ # "curObj[\"_SO\"]=\"D\";curObj[\"viewName\"]=\"All_Requests\";"
176
+ curObj = Hash.new
177
+ curobj_raw.split(";").each { |c| eval(c) }
178
+ curObj["ID"] = v
179
+ # curObj =>
180
+ # {"_PN"=>"1", "_PL"=>"25", "_TL"=>"28", "globalViewName"=>"All_Requests",
181
+ # "_TI"=>"25", "_FI"=>"1", "_SO"=>"D", "viewName"=>"All_Requests"}
182
+ curObj
183
+ end
184
+
185
+ def get_requests_urls(body)
186
+ urls = body.scan(/href=\"WorkOrder\.do\?woMode=viewWO&woID=\d+&&fromListView=true\"/)
187
+ # drop href=" and ending quot
188
+ urls.each_with_index { |url, i| urls[i] = url["href=\"".size..-2] }
189
+ end
190
+
191
+ private :select_all_requests, :next_page, :get_curobj, :get_requests_urls
192
+ end
193
+
194
+ class Request < MESD
195
+ props = [:name, :author_name, :status, :priority, :create_date, :description, :resolution]
196
+ attr_accessor :id, *props
197
+
198
+ # shortens "request.data(:resolution).resolution" to "request.get_resolution"
199
+ props.each do |prop|
200
+ define_method("get_#{prop.to_s}") do
201
+ request = self.data(prop)
202
+ request.send("#{prop.to_s}")
203
+ end
204
+ end
205
+
206
+ def initialize(args)
207
+ if args[:id]
208
+ @id = args[:id]
209
+ elsif args[:url]
210
+ if args[:url] =~ /WorkOrder\.do\?woMode=viewWO&woID=(?<ID>\d+)&&fromListView=true/
211
+ @id = Regexp.last_match("ID").to_i
212
+ else
213
+ return false
214
+ end
215
+ end
216
+ @session = args[:session]
217
+ true
218
+ end
219
+
220
+ def data(*args)
221
+ return false unless self.id
222
+ if args.size == 0
223
+ only = []
224
+ else
225
+ only = args
226
+ end
227
+ unless session_healthy?(@session)
228
+ @last_error = "session error"
229
+ return false
230
+ end
231
+ props = [
232
+ {
233
+ name: :description,
234
+ url: "WorkOrder.do?woMode=viewWO&woID=#{self.id}",
235
+ search_function: {
236
+ name: "value_between",
237
+ args: ["<td style=\"padding-left:10px;\" colspan=\"3\" valign=\"top\" class=\"fontBlack textareadesc\">", "</td>"],
238
+ },
239
+ post_processing_functions: [:strip],
240
+ },
241
+ {
242
+ name: :resolution,
243
+ url: "AddResolution.do?mode=viewWOResolution&woID=#{self.id}",
244
+ search_function: {
245
+ name: "value_between",
246
+ args: ["<td colspan=\"3\" valign=\"top\" class=\"fontBlack textareadesc\">", "</td>"],
247
+ },
248
+ post_processing_functions: [:strip],
249
+ },
250
+ {
251
+ name: :status,
252
+ url: "WorkOrder.do?woMode=viewWO&woID=#{self.id}",
253
+ search_function: {
254
+ name: "html_parse",
255
+ args: [["css", "#WOHeaderSummary_DIV"], ["css", "#status_PH"], "text"],
256
+ },
257
+ post_processing_functions: [:semicolon_space_value, :symbolize],
258
+ },
259
+ {
260
+ name: :priority,
261
+ url: "WorkOrder.do?woMode=viewWO&woID=#{self.id}",
262
+ search_function: {
263
+ name: "html_parse",
264
+ args: [["css", "#WOHeaderSummary_DIV"], ["css", "#priority_PH"], "text"],
265
+ },
266
+ post_processing_functions: [:semicolon_space_value, :symbolize],
267
+ },
268
+ {
269
+ name: :author_name,
270
+ url: "WorkOrder.do?woMode=viewWO&woID=#{self.id}",
271
+ search_function: {
272
+ name: "html_parse",
273
+ args: [["css", "#requesterName_PH"], "text"],
274
+ },
275
+ },
276
+ {
277
+ name: :create_date,
278
+ url: "WorkOrder.do?woMode=viewWO&woID=#{self.id}",
279
+ search_function: {
280
+ name: "html_parse",
281
+ args: [["css", "#CREATEDTIME_CUR"], "text"],
282
+ },
283
+ post_processing_functions: [:parse_date],
284
+ },
285
+ {
286
+ name: :name,
287
+ url: "WorkOrder.do?woMode=viewWO&woID=#{self.id}",
288
+ search_function: {
289
+ name: "html_parse",
290
+ args: [["css", "#requestSubject_ID"], "text"],
291
+ },
292
+ post_processing_functions: [:strip],
293
+ },
294
+ ]
295
+ props.each do |property|
296
+ next if !only.empty? && !only.include?(property[:name])
297
+ uri = URI("http://#{@session[:host]}:#{@session[:port]}/#{property[:url]}")
298
+ begin
299
+ Net::HTTP.start(uri.host, uri.port) do |http|
300
+ http_request = Net::HTTP::Get.new(uri)
301
+ http_request.add_field("Cookie", "#{@session[:cookie]}")
302
+ http_request = http.request(http_request)
303
+ @current_body = http_request.response.body
304
+ auth_error_pos = @current_body.index("AuthError")
305
+ if auth_error_pos
306
+ @last_error = "auth error"
307
+ return false
308
+ end
309
+ permitions_error_pos = @current_body.index("Request does not fall under your permitted scope")
310
+ if permitions_error_pos
311
+ @last_error = "no permitions error"
312
+ return false
313
+ end
314
+ operational_error_pos = @current_body.index("failurebox")
315
+ if operational_error_pos
316
+ @last_error = "operational error"
317
+ return false
318
+ end
319
+ value = self.method(property[:search_function][:name]).call(property[:search_function][:args])
320
+ if property[:post_processing_functions]
321
+ functions = property[:post_processing_functions]
322
+ functions.each do |function|
323
+ if value.methods.include?(function)
324
+ value = value.method(function).call
325
+ elsif self.private_methods.include?(function)
326
+ value = self.method(function).call(value)
327
+ end
328
+ end
329
+ end
330
+ self.send("#{property[:name]}=", value)
331
+ end
332
+ rescue *EXCEPTIONS => @last_error
333
+ end
334
+ end
335
+ self
336
+ end
337
+
338
+ def html_parse(steps)
339
+ require "nokogiri"
340
+ value = Nokogiri::HTML(@current_body)
341
+ Array(steps).each { |step| value = value.send(*step) }
342
+ value
343
+ end
344
+
345
+ def value_between(bounds)
346
+ search_start_pos = @current_body.index(bounds[0])
347
+ return "" unless search_start_pos
348
+ search_end_pos = @current_body.index(bounds[1], search_start_pos)
349
+ @current_body[search_start_pos + bounds[0].size..search_end_pos-1].force_encoding("UTF-8")
350
+ end
351
+
352
+ def semicolon_space_value(value)
353
+ value.strip[/:(.*)/m, 1].strip
354
+ end
355
+
356
+ def parse_date(date)
357
+ require "date"
358
+ DateTime.parse(date)
359
+ end
360
+
361
+ def symbolize(value)
362
+ matching = {
363
+ # status
364
+ :open => ["Открыта", "Open"],
365
+ :on_hold => ["Ожидание", "On Hold"],
366
+ :resolved => ["Решена", "Resolved"],
367
+ :closed => ["Закрыта", "Closed"],
368
+ :rejected => ["Отклонена", ""],
369
+ # priority
370
+ :minimal => ["Минимальный", ""],
371
+ :low => ["Низкий", "Low"],
372
+ :normal => ["", "Normal"],
373
+ :medium => ["Средний", "Medium"],
374
+ :high => ["Высокий", "High"],
375
+ :highest => ["Наивысший", ""],
376
+ }
377
+ matching.each do |result, candidates|
378
+ return result if candidates.include?(value)
379
+ end
380
+ value.to_sym
381
+ end
382
+
383
+ private :html_parse, :value_between, :semicolon_space_value, :parse_date, :symbolize
384
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: me_sd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3beta
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Morozov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ description: Introduces 'MESD' class that works with ManageEngine ServiceDesk Plus
28
+ without API access.
29
+ email: ntcomp12@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/me_sd.rb
35
+ homepage: https://github.com/kengho/me_sd
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">"
51
+ - !ruby/object:Gem::Version
52
+ version: 1.3.1
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 2.5.1
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: ManageEngine ServiceDesk Plus gem
59
+ test_files: []