pxmyportal 0.0.3 → 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/pxmyportal/agent.rb +75 -117
- data/lib/pxmyportal/authentication.rb +58 -0
- data/lib/pxmyportal/command.rb +5 -5
- data/lib/pxmyportal/cookie.rb +45 -26
- data/lib/pxmyportal/document_downloader.rb +39 -0
- data/lib/pxmyportal/error.rb +15 -0
- data/lib/pxmyportal/http_client.rb +37 -0
- data/lib/pxmyportal/page.rb +27 -3
- data/lib/pxmyportal/payslip.rb +8 -14
- data/lib/pxmyportal/payslip_list.rb +36 -0
- data/lib/pxmyportal/token_issuer.rb +46 -0
- data/lib/pxmyportal/version.rb +4 -4
- data/lib/pxmyportal/xdg.rb +18 -1
- data/lib/pxmyportal.rb +3 -3
- metadata +16 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c9e7264c0b9326ead4b20b960416715c49a7e0a6c780fdb1ad8c938a4ba215d
|
4
|
+
data.tar.gz: 328ee111614bafbe6d0a44888d5d9256ce1126ccd97c3a99ca5d953d91957a0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 231587763356fc6ebb4b7c58b49ab9dbfa786714c9f78a22d1bac6134c74a6ef5ad937734fa4c9dfe0d3767cbb8edffc2a980a0a5ed25691ab02fea2ba70ae36
|
7
|
+
data.tar.gz: f5c7045478a23f82b1e24738565f79ec345f520a301b185ed62d7c5894b7d770ab1d376c12f6f81c11ca1aab759b6e3618974b85a80fcb4b7430139ac81e8348
|
data/CHANGELOG.md
CHANGED
data/lib/pxmyportal/agent.rb
CHANGED
@@ -1,71 +1,72 @@
|
|
1
1
|
# Copyright (C) 2025 gemmaro
|
2
|
-
|
2
|
+
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify
|
4
4
|
# it under the terms of the GNU General Public License as published by
|
5
5
|
# the Free Software Foundation, either version 3 of the License, or
|
6
6
|
# (at your option) any later version.
|
7
|
-
|
7
|
+
#
|
8
8
|
# This program is distributed in the hope that it will be useful,
|
9
9
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
10
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
11
|
# GNU General Public License for more details.
|
12
|
-
|
12
|
+
#
|
13
13
|
# You should have received a copy of the GNU General Public License
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
15
|
|
16
16
|
require "yaml"
|
17
|
-
require "net/http"
|
18
|
-
require "nokogiri"
|
19
17
|
require "logger"
|
20
18
|
require "set"
|
21
19
|
require_relative "payslip"
|
22
20
|
require_relative "error"
|
23
|
-
require_relative "cookie"
|
24
21
|
require_relative "page"
|
25
22
|
|
26
23
|
class PXMyPortal::Agent
|
27
|
-
def let_redirect
|
28
|
-
|
29
|
-
|
30
|
-
@
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
response => Net::HTTPFound
|
39
|
-
rescue => e
|
40
|
-
File.write(File.join(PXMyPortal::XDG::CACHE_DIR, "debug", "let_redirect.html"),
|
41
|
-
response.body)
|
42
|
-
raise e
|
43
|
-
end
|
44
|
-
|
45
|
-
@page = PXMyPortal::Page.from_path(response["location"]) \
|
46
|
-
or raise PXMyPortal::Error, "unexpected location #{location}"
|
47
|
-
@cookie.accept(response, url: build_url(@page.path))
|
48
|
-
self
|
24
|
+
def let_redirect(path: PXMyPortal::Page::BASEPATH)
|
25
|
+
@request_verification_token ||= @token_issuer.get
|
26
|
+
|
27
|
+
@page = PXMyPortal::Authentication.new(
|
28
|
+
path:,
|
29
|
+
user: @user,
|
30
|
+
password: @password,
|
31
|
+
token: @request_verification_token,
|
32
|
+
http: @http,
|
33
|
+
logger: @logger,
|
34
|
+
).post
|
49
35
|
end
|
50
36
|
|
51
37
|
def save_payslips
|
38
|
+
let_redirect
|
39
|
+
|
52
40
|
existing_payslips = (YAML.load_file(@payslips_path) rescue []) || []
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
41
|
+
|
42
|
+
pages = Set[PXMyPortal::Page::BONUS]
|
43
|
+
unless @bonus_only
|
44
|
+
pages << @page
|
45
|
+
end
|
46
|
+
|
47
|
+
pages.each do |page|
|
48
|
+
let_redirect(path: page.path)
|
49
|
+
payslips = payslips_for_page(page)
|
50
|
+
|
51
|
+
payslips.each do |payslip|
|
52
|
+
if !@force && existing_payslips&.find { |candidate| payslip == candidate }
|
53
|
+
@logger.info("skipping") { payslip }
|
54
|
+
next
|
55
|
+
end
|
56
|
+
path = @page.confirm_path
|
57
|
+
data = PXMyPortal::DocumentDownloader.new(
|
58
|
+
path:,
|
59
|
+
form_data: payslip.form_data,
|
60
|
+
http: @http,
|
61
|
+
logger: @logger,
|
62
|
+
).post
|
63
|
+
|
64
|
+
path = File.join(@payslip_dir, payslip.filename)
|
65
|
+
FileUtils.mkdir_p(@payslip_dir)
|
66
|
+
@logger.info("saving payslip...") { path }
|
67
|
+
File.write(path, data) unless @test
|
68
|
+
existing_payslips << payslip.metadata
|
57
69
|
end
|
58
|
-
path = @page.confirm_path
|
59
|
-
request = Net::HTTP::Post.new(path)
|
60
|
-
@cookie.provide(request, url: build_url(path))
|
61
|
-
request.form_data = payslip.form_data
|
62
|
-
response = http.request(request)
|
63
|
-
response => Net::HTTPOK
|
64
|
-
|
65
|
-
FileUtils.mkdir_p(payslip.directory) unless File.directory?(payslip.directory)
|
66
|
-
@logger.info("saving payslip...") { payslip.filename }
|
67
|
-
File.write(payslip.filename, response.body) unless @test
|
68
|
-
existing_payslips << payslip.metadata
|
69
70
|
end
|
70
71
|
|
71
72
|
File.open(payslips_path, "w") { |file| YAML.dump(existing_payslips, file) } \
|
@@ -86,97 +87,54 @@ class PXMyPortal::Agent
|
|
86
87
|
@created_payslips_path = @payslips_path
|
87
88
|
end
|
88
89
|
|
89
|
-
def payslips
|
90
|
-
return @payslips if @payslips
|
91
|
-
|
92
|
-
pages = Set[PXMyPortal::Page::BONUS]
|
93
|
-
unless @bonus_only
|
94
|
-
pages << @page
|
95
|
-
end
|
96
|
-
@logger.debug("pages") { pages }
|
97
|
-
|
98
|
-
@payslips = []
|
99
|
-
pages.each do |page|
|
100
|
-
@payslips.concat(payslips_for_page(page))
|
101
|
-
end
|
102
|
-
@logger.warn("no payslips") if @payslips.empty?
|
103
|
-
@logger.debug("payslips") { @payslips }
|
104
|
-
@payslips
|
105
|
-
end
|
106
|
-
|
107
90
|
def payslips_for_page(page)
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
File.write(page.cache_path, response.body)
|
117
|
-
page.rows(response.body)
|
118
|
-
.map { |row| PXMyPortal::Payslip.from_row(row, directory: @payslip_dir) }
|
119
|
-
end
|
120
|
-
|
121
|
-
def request_verification_token
|
122
|
-
return @request_verification_token if @request_verification_token
|
123
|
-
|
124
|
-
@debug_http and http.set_debug_output($stderr)
|
125
|
-
http.start
|
126
|
-
path = File.join(PXMyPortal::Page::BASEPATH, "Auth/Login")
|
127
|
-
query = @company
|
128
|
-
response = http.get("#{path}?#{query}")
|
129
|
-
response => Net::HTTPOK
|
130
|
-
|
131
|
-
@cookie.load
|
132
|
-
@cookie.accept(response, url: build_url(path, query:))
|
133
|
-
|
134
|
-
document = Nokogiri::HTML(response.body)
|
135
|
-
token = <<~XPATH
|
136
|
-
//form//input[ @type='hidden'
|
137
|
-
and @name='__RequestVerificationToken' ]
|
138
|
-
/@value
|
139
|
-
XPATH
|
140
|
-
document.xpath(token) => [token]
|
141
|
-
token or raise Error, token
|
142
|
-
@request_verification_token = token
|
143
|
-
end
|
144
|
-
|
145
|
-
def build_url(path, query: nil)
|
146
|
-
URI::HTTPS.build(host: PXMyPortal::HOST, path:, query:)
|
91
|
+
data = PXMyPortal::PayslipList.new(
|
92
|
+
path: page.path,
|
93
|
+
logger: @logger,
|
94
|
+
http: @http,
|
95
|
+
).get
|
96
|
+
File.write(page.cache_path, data)
|
97
|
+
page.rows(data)
|
98
|
+
.map { |row| PXMyPortal::Payslip.from_row(row) }
|
147
99
|
end
|
148
100
|
|
149
101
|
def initialize(debug: false,
|
150
|
-
|
151
|
-
payslips_path: File.join(ENV["XDG_DATA_HOME"],
|
152
|
-
"pxmyportal", "payslips.yaml"),
|
102
|
+
payslips_path: File.join(PXMyPortal::XDG::DATA_DIR, "payslips.yaml"),
|
153
103
|
company:,
|
154
104
|
user:,
|
155
105
|
password:,
|
156
106
|
test: false,
|
157
|
-
payslip_dir:
|
107
|
+
payslip_dir: PXMyPortal::XDG::DOC_DIR,
|
158
108
|
bonus_only: false,
|
159
|
-
debug_http: false
|
109
|
+
debug_http: false,
|
110
|
+
force: false)
|
160
111
|
|
161
112
|
@company = company
|
162
113
|
@user = user
|
163
114
|
@password = password
|
164
|
-
@debug = debug
|
165
115
|
@payslips_path = payslips_path
|
166
116
|
@test = test
|
167
117
|
@payslip_dir = payslip_dir
|
168
118
|
@bonus_only = bonus_only
|
169
119
|
@debug_http = debug_http
|
170
|
-
|
171
|
-
|
172
|
-
@
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
120
|
+
@force = force
|
121
|
+
|
122
|
+
@logger = Logger.new($stderr, level: :info)
|
123
|
+
@logger.level = :debug if @debug
|
124
|
+
|
125
|
+
@http = PXMyPortal::HTTPClient.new(
|
126
|
+
debug: @debug_http,
|
127
|
+
logger: @logger,
|
128
|
+
)
|
129
|
+
@token_issuer = PXMyPortal::TokenIssuer.new(
|
130
|
+
http: @http,
|
131
|
+
company: @company,
|
132
|
+
)
|
181
133
|
end
|
182
134
|
end
|
135
|
+
|
136
|
+
require_relative "http_client"
|
137
|
+
require_relative "token_issuer"
|
138
|
+
require_relative "document_downloader"
|
139
|
+
require_relative "authentication"
|
140
|
+
require_relative "payslip_list"
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require "net/http"
|
17
|
+
require_relative "xdg"
|
18
|
+
require_relative "page"
|
19
|
+
|
20
|
+
class PXMyPortal::Authentication
|
21
|
+
def initialize(path:, user:, password:, token:, http:, logger:)
|
22
|
+
@path = path
|
23
|
+
@user = user
|
24
|
+
@password = password
|
25
|
+
@token = token
|
26
|
+
@http = http
|
27
|
+
@logger = logger
|
28
|
+
|
29
|
+
@request = Net::HTTP::Post.new(@path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def post
|
33
|
+
@http.provide_cookie(@request)
|
34
|
+
|
35
|
+
@request.form_data = {
|
36
|
+
LoginId: @user,
|
37
|
+
Password: @password,
|
38
|
+
"__RequestVerificationToken" => @token,
|
39
|
+
}
|
40
|
+
response = @http.request(@request)
|
41
|
+
begin
|
42
|
+
response => Net::HTTPFound | Net::HTTPOK
|
43
|
+
rescue => e
|
44
|
+
File.write(File.join(PXMyPortal::XDG::CACHE_DIR, "authentication.html"),
|
45
|
+
response.body)
|
46
|
+
raise e
|
47
|
+
end
|
48
|
+
|
49
|
+
@logger.debug("response") { response.to_hash }
|
50
|
+
page = PXMyPortal::Page.from_path(response["location"] || @path)
|
51
|
+
unless page
|
52
|
+
@logger.error("location") { response["location"] }
|
53
|
+
raise PXMyPortal::Error, "unexpected location"
|
54
|
+
end
|
55
|
+
@http.accept_cookie(response)
|
56
|
+
page
|
57
|
+
end
|
58
|
+
end
|
data/lib/pxmyportal/command.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# Copyright (C) 2025 gemmaro
|
2
|
-
|
2
|
+
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify
|
4
4
|
# it under the terms of the GNU General Public License as published by
|
5
5
|
# the Free Software Foundation, either version 3 of the License, or
|
6
6
|
# (at your option) any later version.
|
7
|
-
|
7
|
+
#
|
8
8
|
# This program is distributed in the hope that it will be useful,
|
9
9
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
10
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
11
|
# GNU General Public License for more details.
|
12
|
-
|
12
|
+
#
|
13
13
|
# You should have received a copy of the GNU General Public License
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
15
|
|
@@ -25,17 +25,17 @@ class PXMyPortal::Command
|
|
25
25
|
|
26
26
|
parser = OptionParser.new
|
27
27
|
parser.on("--debug") { options[:debug] = true }
|
28
|
-
parser.on("--cookie-jar=PATH") {
|
28
|
+
parser.on("--cookie-jar=PATH") { warn "deprecated option" }
|
29
29
|
parser.on("--payslips=PATH",
|
30
30
|
"database file for previously stored payslips") { |path|
|
31
31
|
options[:payslips_path] = path }
|
32
32
|
parser.on("--payslip-dir=PATH") { |path| options[:payslip_dir] = path }
|
33
33
|
parser.on("--bonus-only") { options[:bonus_only] = true }
|
34
34
|
parser.on("--debug-http") { options[:debug_http] = true }
|
35
|
+
parser.on("--force") { options[:force] = true }
|
35
36
|
parser.parse!
|
36
37
|
|
37
38
|
agent = PXMyPortal::Agent.new(**options)
|
38
|
-
agent.let_redirect
|
39
39
|
agent.save_payslips
|
40
40
|
end
|
41
41
|
end
|
data/lib/pxmyportal/cookie.rb
CHANGED
@@ -1,37 +1,56 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require "set"
|
3
17
|
|
4
18
|
class PXMyPortal::Cookie
|
5
|
-
def initialize(
|
6
|
-
@
|
19
|
+
def initialize(logger:)
|
20
|
+
@set = Set.new
|
7
21
|
@logger = logger
|
8
|
-
|
9
|
-
@jar = HTTP::CookieJar.new
|
10
22
|
end
|
11
23
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# Previously accept_cookie.
|
17
|
-
def accept(response, url:)
|
18
|
-
response.get_fields("Set-Cookie").each { |value| @jar.parse(value, url) }
|
19
|
-
@jar.save(jar_path)
|
20
|
-
end
|
24
|
+
def accept(response)
|
25
|
+
fields = Set[*response.get_fields("Set-Cookie"),
|
26
|
+
*response.get_fields("set-cookie")]
|
21
27
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
fields.each do |field|
|
29
|
+
field.split(/; +/).each do |pair|
|
30
|
+
case pair
|
31
|
+
when "path=/",
|
32
|
+
"secure",
|
33
|
+
"HttpOnly",
|
34
|
+
"SameSite=Lax",
|
35
|
+
"selectedPage=pc",
|
36
|
+
/\Aexpires=/
|
37
|
+
next
|
38
|
+
end
|
39
|
+
key, = pair.split('=')
|
40
|
+
case key
|
41
|
+
when "ASP.NET_SessionId",
|
42
|
+
".AspNet.ApplicationCookie",
|
43
|
+
"qs",
|
44
|
+
/\A__RequestVerificationToken_([A-Za-z0-9]+)/
|
45
|
+
else
|
46
|
+
raise PXMyPortal::Error, "unknown cookie entry #{pair.inspect}"
|
47
|
+
end
|
48
|
+
@set << pair
|
49
|
+
end
|
29
50
|
end
|
30
|
-
@created_jar_path = @jar_path
|
31
51
|
end
|
32
52
|
|
33
|
-
|
34
|
-
|
35
|
-
request["Cookie"] = HTTP::Cookie.cookie_value(@jar.cookies(url))
|
53
|
+
def provide(request)
|
54
|
+
request["Cookie"] = @set.join(';')
|
36
55
|
end
|
37
56
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require "net/http"
|
17
|
+
|
18
|
+
class PXMyPortal::DocumentDownloader
|
19
|
+
def initialize(path:, form_data:, logger:, http:)
|
20
|
+
@path = path
|
21
|
+
@form_data = form_data
|
22
|
+
@logger = logger
|
23
|
+
@http = http
|
24
|
+
|
25
|
+
@request = Net::HTTP::Post.new(@path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def post
|
29
|
+
@http.provide_cookie(@request)
|
30
|
+
@request.form_data = @form_data
|
31
|
+
@logger.debug("request") { @request }
|
32
|
+
|
33
|
+
response = @http.request(@request)
|
34
|
+
response => Net::HTTPOK
|
35
|
+
@logger.debug("response") { response.to_hash }
|
36
|
+
response.to_hash["content-type"] => ["application/pdf"]
|
37
|
+
response.body
|
38
|
+
end
|
39
|
+
end
|
data/lib/pxmyportal/error.rb
CHANGED
@@ -1 +1,16 @@
|
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
1
16
|
PXMyPortal::Error = Class.new(StandardError)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require "net/http"
|
17
|
+
require "forwardable"
|
18
|
+
require_relative "cookie"
|
19
|
+
|
20
|
+
class PXMyPortal::HTTPClient
|
21
|
+
def initialize(debug: false, logger:)
|
22
|
+
@http = Net::HTTP.new(PXMyPortal::HOST, Net::HTTP.https_default_port)
|
23
|
+
@http.use_ssl = true
|
24
|
+
@http.set_debug_output($stderr) if debug
|
25
|
+
|
26
|
+
@cookie = PXMyPortal::Cookie.new(logger:)
|
27
|
+
end
|
28
|
+
|
29
|
+
def start
|
30
|
+
@http.started? or @http.start
|
31
|
+
end
|
32
|
+
|
33
|
+
extend Forwardable
|
34
|
+
def_delegators :@http, :get, :request, :set_debug_output, :started?
|
35
|
+
def_delegator :@cookie, :provide, :provide_cookie
|
36
|
+
def_delegator :@cookie, :accept, :accept_cookie
|
37
|
+
end
|
data/lib/pxmyportal/page.rb
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require "fileutils"
|
17
|
+
require "nokogiri"
|
18
|
+
|
1
19
|
# Previously @phase.
|
2
20
|
class PXMyPortal::Page
|
3
21
|
BASEPATH = "/pmpwps/"
|
@@ -21,7 +39,7 @@ class PXMyPortal::Page
|
|
21
39
|
|
22
40
|
def cache_path
|
23
41
|
@cache_path and return @cache_path
|
24
|
-
@cache_path = File.join(PXMyPortal::XDG::CACHE_DIR, "
|
42
|
+
@cache_path = File.join(PXMyPortal::XDG::CACHE_DIR, "page", "#{@cache_filename}.html")
|
25
43
|
|
26
44
|
dir = File.dirname(@cache_path)
|
27
45
|
unless Dir.exist?(dir)
|
@@ -34,6 +52,10 @@ class PXMyPortal::Page
|
|
34
52
|
Nokogiri::HTML(source).xpath(@row_xpath)
|
35
53
|
end
|
36
54
|
|
55
|
+
def inspect
|
56
|
+
"#<Page #{@cache_filename}>"
|
57
|
+
end
|
58
|
+
|
37
59
|
normal_row_xpath = "//*[@id='ContentPlaceHolder1_PayslipGridView']//tr"
|
38
60
|
|
39
61
|
# Previously PAYSLIP_PAGE_PATH_SAMPLE.
|
@@ -42,13 +64,15 @@ class PXMyPortal::Page
|
|
42
64
|
confirm_path: File.join(CLIENT_BASEPATH, "ConfirmSamplePDFFrame"),
|
43
65
|
cache_filename: "sample", row_xpath: normal_row_xpath)
|
44
66
|
|
67
|
+
production_confirm_path = "ConfirmPDFFrame"
|
68
|
+
|
45
69
|
# Previously PAYSLIP_PAGE_PATH_NORMAL.
|
46
70
|
NORMAL = new(path: NORMAL_PATH,
|
47
|
-
confirm_path: File.join(CLIENT_BASEPATH,
|
71
|
+
confirm_path: File.join(CLIENT_BASEPATH, production_confirm_path),
|
48
72
|
cache_filename: "normal", row_xpath: normal_row_xpath)
|
49
73
|
|
50
74
|
# Previously PAYSLIP_PAGE_PATH_BONUS.
|
51
|
-
BONUS = new(path: BONUS_PATH, confirm_path:
|
75
|
+
BONUS = new(path: BONUS_PATH, confirm_path: production_confirm_path,
|
52
76
|
cache_filename: "bonus",
|
53
77
|
row_xpath: "//*[@id='ContentPlaceHolder1_BonusPayslipGridView']//tr")
|
54
78
|
|
data/lib/pxmyportal/payslip.rb
CHANGED
@@ -1,35 +1,33 @@
|
|
1
1
|
# Copyright (C) 2025 gemmaro
|
2
|
-
|
2
|
+
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify
|
4
4
|
# it under the terms of the GNU General Public License as published by
|
5
5
|
# the Free Software Foundation, either version 3 of the License, or
|
6
6
|
# (at your option) any later version.
|
7
|
-
|
7
|
+
#
|
8
8
|
# This program is distributed in the hope that it will be useful,
|
9
9
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
10
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
11
|
# GNU General Public License for more details.
|
12
|
-
|
12
|
+
#
|
13
13
|
# You should have received a copy of the GNU General Public License
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
15
|
|
16
16
|
require_relative "xdg"
|
17
17
|
|
18
18
|
class PXMyPortal::Payslip
|
19
|
-
attr_reader :year_month, :description
|
19
|
+
attr_reader :year_month, :description
|
20
20
|
|
21
|
-
def initialize(year_month:, description:, key1:, key2:, key3
|
22
|
-
directory: PXMyPortal::XDG::DOC_DIR)
|
21
|
+
def initialize(year_month:, description:, key1:, key2:, key3:)
|
23
22
|
@year_month = year_month
|
24
23
|
@description = description
|
25
24
|
@key1 = key1
|
26
25
|
@key2 = key2
|
27
26
|
@key3 = key3
|
28
|
-
@directory = directory
|
29
27
|
end
|
30
28
|
|
31
29
|
def filename
|
32
|
-
@filename ||=
|
30
|
+
@filename ||= "#{@key1}-#{@key2}-#{@key3}.pdf"
|
33
31
|
end
|
34
32
|
|
35
33
|
def form_data
|
@@ -51,7 +49,7 @@ class PXMyPortal::Payslip
|
|
51
49
|
"#<Payslip #{year_month.inspect} #{description.inspect}>"
|
52
50
|
end
|
53
51
|
|
54
|
-
def self.from_row(row
|
52
|
+
def self.from_row(row)
|
55
53
|
row.xpath("./td") => [year_month, description, button]
|
56
54
|
year_month.xpath(".//text()") => [year_month]
|
57
55
|
description.xpath(".//text()") => [description]
|
@@ -66,12 +64,8 @@ class PXMyPortal::Payslip
|
|
66
64
|
key2 = match[:key2]
|
67
65
|
key3 = match[:key3]
|
68
66
|
|
69
|
-
options = {}
|
70
|
-
directory and options[:directory] = directory
|
71
|
-
|
72
67
|
new(year_month: year_month.content,
|
73
68
|
description: description.content,
|
74
|
-
key1:, key2:, key3
|
75
|
-
**options)
|
69
|
+
key1:, key2:, key3:)
|
76
70
|
end
|
77
71
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require "net/http"
|
17
|
+
|
18
|
+
class PXMyPortal::PayslipList
|
19
|
+
def initialize(path:, logger:, http:)
|
20
|
+
@path = path
|
21
|
+
@logger = logger
|
22
|
+
@http = http
|
23
|
+
|
24
|
+
@request = Net::HTTP::Get.new(@path)
|
25
|
+
end
|
26
|
+
|
27
|
+
def get
|
28
|
+
@logger.debug("request") { @request }
|
29
|
+
|
30
|
+
@http.provide_cookie(@request)
|
31
|
+
response = @http.request(@request)
|
32
|
+
@logger.debug("response") { response }
|
33
|
+
response => Net::HTTPOK
|
34
|
+
response.body
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require "net/http"
|
17
|
+
require "nokogiri"
|
18
|
+
require_relative "http_client"
|
19
|
+
require_relative "page"
|
20
|
+
|
21
|
+
class PXMyPortal::TokenIssuer
|
22
|
+
def initialize(http:, company:)
|
23
|
+
@http = http
|
24
|
+
@company = company
|
25
|
+
end
|
26
|
+
|
27
|
+
def get
|
28
|
+
@http.start
|
29
|
+
|
30
|
+
path = File.join(PXMyPortal::Page::BASEPATH, "Auth/Login")
|
31
|
+
query = @company
|
32
|
+
response = @http.get("#{path}?#{query}")
|
33
|
+
response => Net::HTTPOK
|
34
|
+
|
35
|
+
@http.accept_cookie(response)
|
36
|
+
|
37
|
+
document = Nokogiri::HTML(response.body)
|
38
|
+
token = <<~XPATH
|
39
|
+
//form//input[ @type='hidden'
|
40
|
+
and @name='__RequestVerificationToken' ]
|
41
|
+
/@value
|
42
|
+
XPATH
|
43
|
+
document.xpath(token) => [token]
|
44
|
+
token or raise Error, token
|
45
|
+
end
|
46
|
+
end
|
data/lib/pxmyportal/version.rb
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Copyright (C) 2025 gemmaro
|
4
|
-
|
4
|
+
#
|
5
5
|
# This program is free software: you can redistribute it and/or modify
|
6
6
|
# it under the terms of the GNU General Public License as published by
|
7
7
|
# the Free Software Foundation, either version 3 of the License, or
|
8
8
|
# (at your option) any later version.
|
9
|
-
|
9
|
+
#
|
10
10
|
# This program is distributed in the hope that it will be useful,
|
11
11
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
12
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
13
|
# GNU General Public License for more details.
|
14
|
-
|
14
|
+
#
|
15
15
|
# You should have received a copy of the GNU General Public License
|
16
16
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
17
17
|
|
18
18
|
module PXMyPortal
|
19
|
-
VERSION = "0.0
|
19
|
+
VERSION = "0.1.0"
|
20
20
|
end
|
data/lib/pxmyportal/xdg.rb
CHANGED
@@ -1,5 +1,22 @@
|
|
1
|
+
# Copyright (C) 2025 gemmaro
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
1
16
|
class PXMyPortal::XDG
|
2
|
-
CACHE_DIR = File.join(ENV["XDG_CACHE_HOME"], "
|
17
|
+
CACHE_DIR = File.join(ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache"),
|
18
|
+
"pxmyportal")
|
3
19
|
DOC_DIR = File.join(ENV["XDG_DOCUMENTS_DIR"] || File.join(Dir.home, "Documents"),
|
4
20
|
"pxmyportal")
|
21
|
+
DATA_DIR = File.join(ENV["XDG_DATA_HOME"], "pxmyportal")
|
5
22
|
end
|
data/lib/pxmyportal.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# Copyright (C) 2025 gemmaro
|
2
|
-
|
2
|
+
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify
|
4
4
|
# it under the terms of the GNU General Public License as published by
|
5
5
|
# the Free Software Foundation, either version 3 of the License, or
|
6
6
|
# (at your option) any later version.
|
7
|
-
|
7
|
+
#
|
8
8
|
# This program is distributed in the hope that it will be useful,
|
9
9
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
10
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
11
|
# GNU General Public License for more details.
|
12
|
-
|
12
|
+
#
|
13
13
|
# You should have received a copy of the GNU General Public License
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
15
|
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pxmyportal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- gemmaro
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-08-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: http-cookie
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: nokogiri
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -54,17 +40,29 @@ files:
|
|
54
40
|
- exe/pxmyportal
|
55
41
|
- lib/pxmyportal.rb
|
56
42
|
- lib/pxmyportal/agent.rb
|
43
|
+
- lib/pxmyportal/authentication.rb
|
57
44
|
- lib/pxmyportal/command.rb
|
58
45
|
- lib/pxmyportal/cookie.rb
|
46
|
+
- lib/pxmyportal/document_downloader.rb
|
59
47
|
- lib/pxmyportal/error.rb
|
48
|
+
- lib/pxmyportal/http_client.rb
|
60
49
|
- lib/pxmyportal/page.rb
|
61
50
|
- lib/pxmyportal/payslip.rb
|
51
|
+
- lib/pxmyportal/payslip_list.rb
|
52
|
+
- lib/pxmyportal/token_issuer.rb
|
62
53
|
- lib/pxmyportal/version.rb
|
63
54
|
- lib/pxmyportal/xdg.rb
|
64
|
-
homepage:
|
55
|
+
homepage: https://github.com/gemmaro/pxmyportal
|
65
56
|
licenses:
|
66
57
|
- GPL-3.0-or-later
|
67
|
-
metadata:
|
58
|
+
metadata:
|
59
|
+
rubygems_mfa_required: 'true'
|
60
|
+
bug_tracker_uri: https://github.com/gemmaro/pxmyportal/issues
|
61
|
+
changelog_uri: https://github.com/gemmaro/pxmyportal/blob/main/CHANGELOG.md
|
62
|
+
documentation_uri: https://rubydoc.info/gems/pxmyportal
|
63
|
+
homepage_uri: https://github.com/gemmaro/pxmyportal
|
64
|
+
source_code_uri: https://github.com/gemmaro/pxmyportal
|
65
|
+
wiki_uri: https://github.com/gemmaro/pxmyportal/wiki
|
68
66
|
post_install_message:
|
69
67
|
rdoc_options: []
|
70
68
|
require_paths:
|