pdfwalker 1.0.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 +7 -0
- data/COPYING +674 -0
- data/bin/pdfwalker +30 -0
- data/lib/pdfwalker.rb +21 -0
- data/lib/pdfwalker/about.rb +37 -0
- data/lib/pdfwalker/config.rb +120 -0
- data/lib/pdfwalker/file.rb +320 -0
- data/lib/pdfwalker/hexview.rb +84 -0
- data/lib/pdfwalker/imgview.rb +69 -0
- data/lib/pdfwalker/menu.rb +382 -0
- data/lib/pdfwalker/properties.rb +126 -0
- data/lib/pdfwalker/signing.rb +558 -0
- data/lib/pdfwalker/textview.rb +88 -0
- data/lib/pdfwalker/treeview.rb +416 -0
- data/lib/pdfwalker/version.rb +23 -0
- data/lib/pdfwalker/walker.rb +300 -0
- data/lib/pdfwalker/xrefs.rb +75 -0
- metadata +103 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of PDF Walker, a graphical PDF file browser
|
4
|
+
Copyright (C) 2017 Guillaume Delugré.
|
5
|
+
|
6
|
+
PDF Walker is free software: you can redistribute it and/or modify
|
7
|
+
it under the terms of the GNU General Public License as published by
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
9
|
+
(at your option) any later version.
|
10
|
+
|
11
|
+
PDF Walker is distributed in the hope that it will be useful,
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
GNU General Public License for more details.
|
15
|
+
|
16
|
+
You should have received a copy of the GNU General Public License
|
17
|
+
along with PDF Walker. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
=end
|
20
|
+
|
21
|
+
require 'digest/md5'
|
22
|
+
|
23
|
+
module PDFWalker
|
24
|
+
|
25
|
+
class Walker < Window
|
26
|
+
|
27
|
+
def display_file_properties
|
28
|
+
Properties.new(self, @opened) if @opened
|
29
|
+
end
|
30
|
+
|
31
|
+
class Properties < Dialog
|
32
|
+
|
33
|
+
@@acrobat_versions =
|
34
|
+
{
|
35
|
+
1.0 => "1.x",
|
36
|
+
1.1 => "2.x",
|
37
|
+
1.2 => "3.x",
|
38
|
+
1.3 => "4.x",
|
39
|
+
1.4 => "5.x",
|
40
|
+
1.5 => "6.x",
|
41
|
+
1.6 => "7.x",
|
42
|
+
1.7 => "8.x / 9.x / 10.x"
|
43
|
+
}
|
44
|
+
|
45
|
+
def initialize(parent, pdf)
|
46
|
+
super("Document properties", parent, Dialog::MODAL, [Stock::CLOSE, Dialog::RESPONSE_NONE])
|
47
|
+
|
48
|
+
file_frame = create_file_frame(parent)
|
49
|
+
pdf_frame = create_document_frame(pdf)
|
50
|
+
|
51
|
+
vbox.add(file_frame)
|
52
|
+
vbox.add(pdf_frame)
|
53
|
+
|
54
|
+
signal_connect('response') { destroy }
|
55
|
+
|
56
|
+
show_all
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def create_file_frame(parent)
|
62
|
+
file_frame = Frame.new(" File properties ")
|
63
|
+
stat = File.stat(parent.filename)
|
64
|
+
|
65
|
+
labels =
|
66
|
+
[
|
67
|
+
[ "Filename:", parent.filename ],
|
68
|
+
[ "File size:", "#{File.size(parent.filename)} bytes" ],
|
69
|
+
[ "MD5:", Digest::MD5.file(parent.filename).hexdigest ],
|
70
|
+
[ "Read-only:", "#{not stat.writable?}" ],
|
71
|
+
[ "Creation date:", stat.ctime.to_s ],
|
72
|
+
[ "Last modified:", stat.mtime.to_s ]
|
73
|
+
]
|
74
|
+
|
75
|
+
create_table(file_frame, labels)
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_document_frame(pdf)
|
79
|
+
pdf_frame = Frame.new(" PDF properties ")
|
80
|
+
|
81
|
+
pdf_version = pdf.header.to_f
|
82
|
+
if pdf_version >= 1.0 and pdf_version <= 1.7
|
83
|
+
acrobat_version = @@acrobat_versions[pdf_version]
|
84
|
+
else
|
85
|
+
acrobat_version = "unknown version"
|
86
|
+
end
|
87
|
+
|
88
|
+
labels =
|
89
|
+
[
|
90
|
+
[ "Version:", "#{pdf_version} (Acrobat #{acrobat_version})" ],
|
91
|
+
[ "Number of revisions:", "#{pdf.revisions.size}" ],
|
92
|
+
[ "Number of indirect objects:", "#{pdf.indirect_objects.size}" ],
|
93
|
+
[ "Number of pages:", "#{pdf.pages.count}" ],
|
94
|
+
[ "Linearized:", boolean_text(pdf.linearized?) ],
|
95
|
+
[ "Encrypted:", boolean_text(pdf.encrypted?) ],
|
96
|
+
[ "Signed:", boolean_text(pdf.signed?) ],
|
97
|
+
[ "Has usage rights:", boolean_text(pdf.usage_rights?) ],
|
98
|
+
[ "Form:", boolean_text(pdf.form?) ],
|
99
|
+
[ "XFA form:", boolean_text(pdf.xfa_form?) ],
|
100
|
+
[ "Document information:", boolean_text(pdf.document_info?) ],
|
101
|
+
[ "Metadata:", boolean_text(pdf.metadata?) ]
|
102
|
+
]
|
103
|
+
|
104
|
+
create_table(pdf_frame, labels)
|
105
|
+
end
|
106
|
+
|
107
|
+
def create_table(frame, labels)
|
108
|
+
table = Table.new(labels.size + 1, 3)
|
109
|
+
|
110
|
+
labels.each_with_index do |label, row|
|
111
|
+
table.attach(Label.new(label[0]).set_alignment(1,0), 0, 1, row, row + 1, Gtk::FILL, Gtk::SHRINK, 4, 4)
|
112
|
+
table.attach(Label.new(label[1]).set_alignment(0,0), 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
113
|
+
end
|
114
|
+
|
115
|
+
frame.border_width = 5
|
116
|
+
frame.shadow_type = Gtk::SHADOW_IN
|
117
|
+
frame.add(table)
|
118
|
+
end
|
119
|
+
|
120
|
+
def boolean_text(value)
|
121
|
+
value ? 'yes' : 'no'
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,558 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This file is part of PDF Walker, a graphical PDF file browser
|
4
|
+
Copyright (C) 2017 Guillaume Delugré.
|
5
|
+
|
6
|
+
PDF Walker is free software: you can redistribute it and/or modify
|
7
|
+
it under the terms of the GNU General Public License as published by
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
9
|
+
(at your option) any later version.
|
10
|
+
|
11
|
+
PDF Walker is distributed in the hope that it will be useful,
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
GNU General Public License for more details.
|
15
|
+
|
16
|
+
You should have received a copy of the GNU General Public License
|
17
|
+
along with PDF Walker. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
|
19
|
+
=end
|
20
|
+
|
21
|
+
module PDFWalker
|
22
|
+
|
23
|
+
class Walker < Window
|
24
|
+
|
25
|
+
def display_signing_wizard
|
26
|
+
SignWizard.new(self, @opened) if @opened
|
27
|
+
end
|
28
|
+
|
29
|
+
def display_usage_rights_wizard
|
30
|
+
UsageRightsWizard.new(self, @opened) if @opened
|
31
|
+
end
|
32
|
+
|
33
|
+
module SignatureDialogs
|
34
|
+
private
|
35
|
+
|
36
|
+
def open_private_key_dialog(page)
|
37
|
+
file_chooser_dialog('Choose a private RSA key', '*.key', '*.pem', '*.der') do |dialog|
|
38
|
+
begin
|
39
|
+
@pkey = OpenSSL::PKey::RSA.new(File.binread(dialog.filename))
|
40
|
+
|
41
|
+
@pkeyfilename.set_text(dialog.filename)
|
42
|
+
set_page_complete(page, true) if @cert
|
43
|
+
rescue
|
44
|
+
@parent.error("Error loading file '#{File.basename(dialog.filename)}'")
|
45
|
+
|
46
|
+
@pkey = nil
|
47
|
+
@pkeyfilename.text = ""
|
48
|
+
set_page_complete(page, false)
|
49
|
+
ensure
|
50
|
+
@ca = [] # Shall be added to the GUI
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def open_certificate_dialog(page)
|
56
|
+
file_chooser_dialog('Choose a x509 certificate', '*.crt', '*.cer', '*.pem', '*.der') do |dialog|
|
57
|
+
begin
|
58
|
+
@cert = OpenSSL::X509::Certificate.new(File.binread(dialog.filename))
|
59
|
+
|
60
|
+
@certfilename.set_text(dialog.filename)
|
61
|
+
set_page_complete(page, true) if @pkey
|
62
|
+
|
63
|
+
rescue
|
64
|
+
@parent.error("Error loading file '#{File.basename(dialog.filename)}'")
|
65
|
+
|
66
|
+
@cert = nil
|
67
|
+
@certfilename.text = ""
|
68
|
+
set_page_complete(page, false)
|
69
|
+
ensure
|
70
|
+
@ca = [] # Shall be added to the GUI
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def open_pkcs12_file_dialog(page)
|
76
|
+
|
77
|
+
file_chooser_dialog('Open PKCS12 container', '*.pfx', '*.p12') do |dialog|
|
78
|
+
begin
|
79
|
+
p12 = OpenSSL::PKCS12::PKCS12.new(File.binread(dialog.filename), method(:prompt_passphrase))
|
80
|
+
|
81
|
+
raise TypeError, "PKCS12 does not contain a RSA key" unless p12.key.is_a?(OpenSSL::PKey::RSA)
|
82
|
+
raise TypeError, "PKCS12 does not contain a x509 certificate" unless p12.certificate.is_a?(OpenSSL::X509::Certificate)
|
83
|
+
|
84
|
+
@pkey = p12.key
|
85
|
+
@cert = p12.certificate
|
86
|
+
@ca = p12.ca_certs
|
87
|
+
|
88
|
+
@p12filename.set_text(dialog.filename)
|
89
|
+
set_page_complete(page, true)
|
90
|
+
rescue
|
91
|
+
@parent.error("Error loading file '#{File.basename(dialog.filename)}'")
|
92
|
+
|
93
|
+
@pkey, @cert, @ca = nil, nil, []
|
94
|
+
@p12filename.text = ""
|
95
|
+
set_page_complete(page, false)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_keypair_import_page
|
101
|
+
labels =
|
102
|
+
[
|
103
|
+
[ "Private RSA key:", @pkeyfilename = Entry.new, pkeychoosebtn = Button.new(Gtk::Stock::OPEN) ],
|
104
|
+
[ "Public certificate:", @certfilename = Entry.new, certchoosebtn = Button.new(Gtk::Stock::OPEN) ]
|
105
|
+
]
|
106
|
+
|
107
|
+
row = 0
|
108
|
+
table = Table.new(2, 3)
|
109
|
+
labels.each do |lbl, entry, btn|
|
110
|
+
entry.editable = entry.sensitive = false
|
111
|
+
|
112
|
+
table.attach(Label.new(lbl).set_alignment(1,0), 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
113
|
+
table.attach(entry, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
114
|
+
table.attach(btn, 2, 3, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
115
|
+
|
116
|
+
row = row.succ
|
117
|
+
end
|
118
|
+
|
119
|
+
pkeychoosebtn.signal_connect('clicked') { open_private_key_dialog(table) }
|
120
|
+
certchoosebtn.signal_connect('clicked') { open_certificate_dialog(table) }
|
121
|
+
|
122
|
+
append_page(table)
|
123
|
+
set_page_title(table, "Import a public/private key pair")
|
124
|
+
set_page_type(table, Assistant::PAGE_CONTENT)
|
125
|
+
end
|
126
|
+
|
127
|
+
def prompt_passphrase
|
128
|
+
dialog = Dialog.new("Enter passphrase",
|
129
|
+
@parent,
|
130
|
+
Dialog::MODAL,
|
131
|
+
[Stock::OK, Dialog::RESPONSE_OK]
|
132
|
+
)
|
133
|
+
|
134
|
+
pwd_entry = Entry.new.set_visibility(false).show
|
135
|
+
dialog.vbox.pack_start(pwd_entry, true, true, 0)
|
136
|
+
|
137
|
+
pwd = pwd_entry.text if dialog.run == Dialog::RESPONSE_OK
|
138
|
+
|
139
|
+
dialog.destroy
|
140
|
+
pwd.to_s
|
141
|
+
end
|
142
|
+
|
143
|
+
def file_chooser_dialog(title, *patterns)
|
144
|
+
dialog = FileChooserDialog.new(title,
|
145
|
+
@parent,
|
146
|
+
FileChooser::ACTION_OPEN,
|
147
|
+
nil,
|
148
|
+
[Stock::CANCEL, Dialog::RESPONSE_CANCEL],
|
149
|
+
[Stock::OPEN, Dialog::RESPONSE_ACCEPT])
|
150
|
+
|
151
|
+
filter = FileFilter.new
|
152
|
+
patterns.each do |pattern|
|
153
|
+
filter.add_pattern(pattern)
|
154
|
+
end
|
155
|
+
|
156
|
+
dialog.set_filter(filter)
|
157
|
+
|
158
|
+
if dialog.run == Dialog::RESPONSE_ACCEPT
|
159
|
+
yield(dialog)
|
160
|
+
end
|
161
|
+
|
162
|
+
dialog.destroy
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class UsageRightsWizard < Assistant
|
167
|
+
include SignatureDialogs
|
168
|
+
|
169
|
+
def initialize(parent, pdf)
|
170
|
+
super()
|
171
|
+
|
172
|
+
@parent = parent
|
173
|
+
@pkey, @cert = nil, nil
|
174
|
+
|
175
|
+
create_intro_page
|
176
|
+
create_keypair_import_page
|
177
|
+
create_rights_selection_page
|
178
|
+
create_termination_page
|
179
|
+
|
180
|
+
signal_connect('delete_event') { self.destroy }
|
181
|
+
signal_connect('cancel') { self.destroy }
|
182
|
+
signal_connect('close') { self.destroy }
|
183
|
+
|
184
|
+
signal_connect('apply') {
|
185
|
+
rights = selected_usage_rights
|
186
|
+
|
187
|
+
begin
|
188
|
+
pdf.enable_usage_rights(@cert, @pkey, *rights)
|
189
|
+
|
190
|
+
set_page_title(@lastpage, "Usage Rights have been enabled")
|
191
|
+
@msg_status.text = "Usage Rights have been enabled for the current document.\n You should consider saving it now."
|
192
|
+
|
193
|
+
@parent.reload
|
194
|
+
rescue
|
195
|
+
@parent.error("#{$!}: #{$!.backtrace.join($/)}")
|
196
|
+
|
197
|
+
set_page_title(@lastpage, "Usage Rights have not been enabled")
|
198
|
+
@msg_status.text = "An error occured during the signature process."
|
199
|
+
end
|
200
|
+
}
|
201
|
+
|
202
|
+
set_modal(true)
|
203
|
+
|
204
|
+
show_all
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def selected_usage_rights
|
210
|
+
[
|
211
|
+
[ Origami::UsageRights::Rights::DOCUMENT_FULLSAVE, @document_fullsave ],
|
212
|
+
|
213
|
+
[ Origami::UsageRights::Rights::ANNOTS_CREATE, @annots_create ],
|
214
|
+
[ Origami::UsageRights::Rights::ANNOTS_DELETE, @annots_delete ],
|
215
|
+
[ Origami::UsageRights::Rights::ANNOTS_MODIFY, @annots_modify ],
|
216
|
+
[ Origami::UsageRights::Rights::ANNOTS_COPY, @annots_copy ],
|
217
|
+
[ Origami::UsageRights::Rights::ANNOTS_IMPORT, @annots_import ],
|
218
|
+
[ Origami::UsageRights::Rights::ANNOTS_EXPORT, @annots_export ],
|
219
|
+
[ Origami::UsageRights::Rights::ANNOTS_ONLINE, @annots_online ],
|
220
|
+
[ Origami::UsageRights::Rights::ANNOTS_SUMMARYVIEW, @annots_sumview ],
|
221
|
+
|
222
|
+
[ Origami::UsageRights::Rights::FORM_FILLIN, @form_fillin ],
|
223
|
+
[ Origami::UsageRights::Rights::FORM_IMPORT, @form_import ],
|
224
|
+
[ Origami::UsageRights::Rights::FORM_EXPORT, @form_export ],
|
225
|
+
[ Origami::UsageRights::Rights::FORM_SUBMITSTANDALONE, @form_submit ],
|
226
|
+
[ Origami::UsageRights::Rights::FORM_SPAWNTEMPLATE, @form_spawntemplate ],
|
227
|
+
[ Origami::UsageRights::Rights::FORM_BARCODEPLAINTEXT, @form_barcode ],
|
228
|
+
[ Origami::UsageRights::Rights::FORM_ONLINE, @form_online ],
|
229
|
+
|
230
|
+
[ Origami::UsageRights::Rights::SIGNATURE_MODIFY, @signature_modify ],
|
231
|
+
|
232
|
+
[ Origami::UsageRights::Rights::EF_CREATE, @ef_create ],
|
233
|
+
[ Origami::UsageRights::Rights::EF_DELETE, @ef_delete ],
|
234
|
+
[ Origami::UsageRights::Rights::EF_MODIFY, @ef_modify ],
|
235
|
+
[ Origami::UsageRights::Rights::EF_IMPORT, @ef_import ],
|
236
|
+
].select { |_, button| button.active? }
|
237
|
+
.map { |right, _| right }
|
238
|
+
end
|
239
|
+
|
240
|
+
def create_intro_page
|
241
|
+
intro = <<-INTRO.gsub(/^\s+/, '')
|
242
|
+
You are about to enable Usage Rights for the current PDF document.
|
243
|
+
To enable these features, you need to have an Adobe public/private key pair in your possession.
|
244
|
+
|
245
|
+
Make sure you have adobe.crt and adobe.key located in the current directory.
|
246
|
+
INTRO
|
247
|
+
|
248
|
+
vbox = VBox.new(false, 5)
|
249
|
+
vbox.set_border_width(5)
|
250
|
+
|
251
|
+
lbl = Label.new(intro).set_justify(Gtk::JUSTIFY_LEFT).set_wrap(true)
|
252
|
+
|
253
|
+
vbox.pack_start(lbl, true, true, 0)
|
254
|
+
|
255
|
+
append_page(vbox)
|
256
|
+
set_page_title(vbox, "Usage Rights Wizard")
|
257
|
+
set_page_type(vbox, Assistant::PAGE_INTRO)
|
258
|
+
set_page_complete(vbox, true)
|
259
|
+
end
|
260
|
+
|
261
|
+
def create_rights_frame(name)
|
262
|
+
frame = Frame.new(name)
|
263
|
+
frame.border_width = 5
|
264
|
+
frame.shadow_type = Gtk::SHADOW_IN
|
265
|
+
|
266
|
+
frame
|
267
|
+
end
|
268
|
+
|
269
|
+
def create_document_rights_frame
|
270
|
+
frame = create_rights_frame(" Document ")
|
271
|
+
|
272
|
+
@document_fullsave = CheckButton.new("Full Save").set_active(true)
|
273
|
+
|
274
|
+
doc_table = Table.new(1, 2)
|
275
|
+
doc_table.attach(@document_fullsave, 0, 1, 0, 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
276
|
+
frame.add(doc_table)
|
277
|
+
end
|
278
|
+
|
279
|
+
def create_annotations_rights_frame
|
280
|
+
frame = create_rights_frame(" Annotations ")
|
281
|
+
|
282
|
+
annots_table = Table.new(4, 2)
|
283
|
+
annots =
|
284
|
+
[
|
285
|
+
[ @annots_create = CheckButton.new("Create"), @annots_import = CheckButton.new("Import") ],
|
286
|
+
[ @annots_delete = CheckButton.new("Delete"), @annots_export = CheckButton.new("Export") ],
|
287
|
+
[ @annots_modify = CheckButton.new("Modify"), @annots_online = CheckButton.new("Online") ],
|
288
|
+
[ @annots_copy = CheckButton.new("Copy"), @annots_sumview = CheckButton.new("Summary View") ]
|
289
|
+
]
|
290
|
+
|
291
|
+
annots.each_with_index do |cols, row|
|
292
|
+
col1, col2 = cols
|
293
|
+
|
294
|
+
col1.active = true
|
295
|
+
col2.active = true
|
296
|
+
|
297
|
+
annots_table.attach(col1, 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
298
|
+
annots_table.attach(col2, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
299
|
+
end
|
300
|
+
|
301
|
+
frame.add(annots_table)
|
302
|
+
end
|
303
|
+
|
304
|
+
def create_form_rights_frame
|
305
|
+
frame = create_rights_frame(" Forms ")
|
306
|
+
|
307
|
+
form_table = Table.new(4, 2)
|
308
|
+
forms =
|
309
|
+
[
|
310
|
+
[ @form_fillin = CheckButton.new("Fill in"), @form_spawntemplate = CheckButton.new("Spawn template") ],
|
311
|
+
[ @form_import = CheckButton.new("Import"), @form_barcode = CheckButton.new("Barcode plaintext") ],
|
312
|
+
[ @form_export = CheckButton.new("Export"), @form_online = CheckButton.new("Online") ],
|
313
|
+
[ @form_submit = CheckButton.new("Submit stand-alone"), nil ]
|
314
|
+
]
|
315
|
+
|
316
|
+
forms.each_with_index do |cols, row|
|
317
|
+
col1, col2 = cols
|
318
|
+
|
319
|
+
col1.active = true
|
320
|
+
col2.active = true unless col2.nil?
|
321
|
+
|
322
|
+
form_table.attach(col1, 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
323
|
+
form_table.attach(col2, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) unless col2.nil?
|
324
|
+
end
|
325
|
+
|
326
|
+
frame.add(form_table)
|
327
|
+
end
|
328
|
+
|
329
|
+
def create_signature_rights_frame
|
330
|
+
frame = create_rights_frame(" Signature ")
|
331
|
+
|
332
|
+
@signature_modify = CheckButton.new("Modify").set_active(true)
|
333
|
+
|
334
|
+
signature_table = Table.new(1, 2)
|
335
|
+
signature_table.attach(@signature_modify, 0, 1, 0, 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
336
|
+
frame.add(signature_table)
|
337
|
+
end
|
338
|
+
|
339
|
+
def create_embedded_files_rights_frame
|
340
|
+
frame = create_rights_frame(" Embedded files ")
|
341
|
+
|
342
|
+
ef_table = Table.new(2,2)
|
343
|
+
ef_buttons =
|
344
|
+
[
|
345
|
+
[ @ef_create = CheckButton.new("Create"), @ef_modify = CheckButton.new("Modify") ],
|
346
|
+
[ @ef_delete = CheckButton.new("Delete"), @ef_import = CheckButton.new("Import") ]
|
347
|
+
]
|
348
|
+
|
349
|
+
ef_buttons.each_with_index do |cols, row|
|
350
|
+
col1, col2 = cols
|
351
|
+
|
352
|
+
col1.active = true
|
353
|
+
col2.active = true
|
354
|
+
|
355
|
+
ef_table.attach(col1, 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
356
|
+
ef_table.attach(col2, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
357
|
+
end
|
358
|
+
|
359
|
+
frame.add(ef_table)
|
360
|
+
end
|
361
|
+
|
362
|
+
def create_rights_selection_page
|
363
|
+
vbox = VBox.new(false, 5)
|
364
|
+
|
365
|
+
vbox.add create_document_rights_frame
|
366
|
+
vbox.add create_annotations_rights_frame
|
367
|
+
vbox.add create_form_rights_frame
|
368
|
+
vbox.add create_signature_rights_frame
|
369
|
+
vbox.add create_embedded_files_rights_frame
|
370
|
+
|
371
|
+
append_page(vbox)
|
372
|
+
set_page_title(vbox, "Select Usage Rights to enable")
|
373
|
+
set_page_type(vbox, Assistant::PAGE_CONFIRM)
|
374
|
+
set_page_complete(vbox, true)
|
375
|
+
end
|
376
|
+
|
377
|
+
def create_termination_page
|
378
|
+
@lastpage = VBox.new(false, 5)
|
379
|
+
|
380
|
+
@msg_status = Label.new
|
381
|
+
@lastpage.pack_start(@msg_status, true, true, 0)
|
382
|
+
|
383
|
+
append_page(@lastpage)
|
384
|
+
set_page_title(@lastpage, "Usage Rights have not been enabled")
|
385
|
+
set_page_type(@lastpage, Assistant::PAGE_SUMMARY)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
class SignWizard < Assistant
|
390
|
+
include SignatureDialogs
|
391
|
+
|
392
|
+
INTRO_PAGE = 0
|
393
|
+
KEY_SELECT_PAGE = 1
|
394
|
+
PKCS12_IMPORT_PAGE = 2
|
395
|
+
KEYPAIR_IMPORT_PAGE = 3
|
396
|
+
SIGNATURE_INFO_PAGE = 4
|
397
|
+
SIGNATURE_RESULT_PAGE = 5
|
398
|
+
|
399
|
+
def initialize(parent, pdf)
|
400
|
+
super()
|
401
|
+
|
402
|
+
@parent = parent
|
403
|
+
|
404
|
+
@pkey, @cert, @ca = nil, nil, []
|
405
|
+
|
406
|
+
create_intro_page
|
407
|
+
create_key_selection_page
|
408
|
+
create_pkcs12_import_page
|
409
|
+
create_keypair_import_page
|
410
|
+
create_signature_info_page
|
411
|
+
create_termination_page
|
412
|
+
|
413
|
+
set_forward_page_func { |current_page|
|
414
|
+
case current_page
|
415
|
+
when KEY_SELECT_PAGE
|
416
|
+
if @p12button.active? then PKCS12_IMPORT_PAGE else KEYPAIR_IMPORT_PAGE end
|
417
|
+
|
418
|
+
when PKCS12_IMPORT_PAGE, KEYPAIR_IMPORT_PAGE
|
419
|
+
SIGNATURE_INFO_PAGE
|
420
|
+
|
421
|
+
else current_page.succ
|
422
|
+
end
|
423
|
+
}
|
424
|
+
|
425
|
+
signal_connect('delete_event') { self.destroy }
|
426
|
+
signal_connect('cancel') { self.destroy }
|
427
|
+
signal_connect('close') { self.destroy }
|
428
|
+
|
429
|
+
signal_connect('apply') {
|
430
|
+
location = @location.text.empty? ? nil : @location.text
|
431
|
+
contact = @email.text.empty? ? nil : @email.text
|
432
|
+
reason = @reason.text.empty? ? nil : @reason.text
|
433
|
+
|
434
|
+
begin
|
435
|
+
pdf.sign(@cert, @pkey,
|
436
|
+
ca: @ca,
|
437
|
+
location: location,
|
438
|
+
contact: contact,
|
439
|
+
reason: reason)
|
440
|
+
|
441
|
+
set_page_title(@lastpage, "Document has been signed")
|
442
|
+
@msg_status.text = "The document has been signed.\n You should consider saving it now."
|
443
|
+
|
444
|
+
@parent.reload
|
445
|
+
rescue
|
446
|
+
@parent.error("#{$!}: #{$!.backtrace.join($/)}")
|
447
|
+
|
448
|
+
set_page_title(@lastpage, "Document has not been signed")
|
449
|
+
@msg_status.text = "An error occured during the signature process."
|
450
|
+
end
|
451
|
+
}
|
452
|
+
|
453
|
+
set_modal(true)
|
454
|
+
|
455
|
+
show_all
|
456
|
+
end
|
457
|
+
|
458
|
+
private
|
459
|
+
|
460
|
+
def create_intro_page
|
461
|
+
intro = <<-INTRO.gsub(/^\s+/, '')
|
462
|
+
You are about to sign the current PDF document.
|
463
|
+
Once the document will be signed, no further modification will be allowed.
|
464
|
+
|
465
|
+
The signature process is based on assymetric cryptography, so you will basically need a public/private RSA key pair (between 1024 and 4096 bits).
|
466
|
+
INTRO
|
467
|
+
|
468
|
+
vbox = VBox.new(false, 5)
|
469
|
+
vbox.set_border_width(5)
|
470
|
+
|
471
|
+
lbl = Label.new(intro).set_justify(Gtk::JUSTIFY_LEFT).set_wrap(true)
|
472
|
+
|
473
|
+
vbox.pack_start(lbl, true, true, 0)
|
474
|
+
|
475
|
+
append_page(vbox)
|
476
|
+
set_page_title(vbox, "Signature Wizard")
|
477
|
+
set_page_type(vbox, Assistant::PAGE_INTRO)
|
478
|
+
set_page_complete(vbox, true)
|
479
|
+
end
|
480
|
+
|
481
|
+
def create_key_selection_page
|
482
|
+
vbox = VBox.new(false, 5)
|
483
|
+
|
484
|
+
@rawbutton = RadioButton.new("Import keys from separate PEM/DER encoded files")
|
485
|
+
@p12button = RadioButton.new(@rawbutton, "Import keys from a PKCS12 container")
|
486
|
+
|
487
|
+
vbox.pack_start(@rawbutton, true, true, 0)
|
488
|
+
vbox.pack_start(@p12button, true, true, 0)
|
489
|
+
|
490
|
+
append_page(vbox)
|
491
|
+
set_page_title(vbox, "Choose a key importation method")
|
492
|
+
set_page_type(vbox, Assistant::PAGE_CONTENT)
|
493
|
+
set_page_complete(vbox, true)
|
494
|
+
end
|
495
|
+
|
496
|
+
def create_pkcs12_import_page
|
497
|
+
vbox = VBox.new(false, 5)
|
498
|
+
|
499
|
+
hbox = HBox.new(false, 5)
|
500
|
+
vbox.pack_start(hbox, true, false, 10)
|
501
|
+
|
502
|
+
@p12filename = Entry.new.set_editable(false).set_sensitive(false)
|
503
|
+
choosebtn = Button.new(Gtk::Stock::OPEN)
|
504
|
+
|
505
|
+
choosebtn.signal_connect('clicked') { open_pkcs12_file_dialog(vbox) }
|
506
|
+
|
507
|
+
hbox.pack_start(@p12filename, true, true, 5)
|
508
|
+
hbox.pack_start(choosebtn, false, false, 5)
|
509
|
+
|
510
|
+
append_page(vbox)
|
511
|
+
set_page_title(vbox, "Import a PKCS12 container")
|
512
|
+
set_page_type(vbox, Assistant::PAGE_CONTENT)
|
513
|
+
end
|
514
|
+
|
515
|
+
def create_signature_info_page
|
516
|
+
vbox = VBox.new(false, 5)
|
517
|
+
|
518
|
+
lbl = Label.new("Here are a few optional information you can add with your signature.")
|
519
|
+
vbox.pack_start(lbl, true, true, 0)
|
520
|
+
|
521
|
+
labels =
|
522
|
+
[
|
523
|
+
[ "Location:", @location = Entry.new ],
|
524
|
+
[ "Contact:", @email = Entry.new ],
|
525
|
+
[ "Reason:", @reason = Entry.new ]
|
526
|
+
]
|
527
|
+
|
528
|
+
row = 0
|
529
|
+
table = Table.new(4, 3)
|
530
|
+
labels.each do |label|
|
531
|
+
table.attach(Label.new(label[0]).set_alignment(1,0), 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
532
|
+
table.attach(label[1], 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4)
|
533
|
+
|
534
|
+
row = row.succ
|
535
|
+
end
|
536
|
+
|
537
|
+
vbox.pack_start(table, true, true, 0)
|
538
|
+
|
539
|
+
append_page(vbox)
|
540
|
+
set_page_title(vbox, "Fill in signature details")
|
541
|
+
set_page_type(vbox, Assistant::PAGE_CONFIRM)
|
542
|
+
set_page_complete(vbox, true)
|
543
|
+
end
|
544
|
+
|
545
|
+
def create_termination_page
|
546
|
+
@lastpage = VBox.new(false, 5)
|
547
|
+
|
548
|
+
@msg_status = Label.new
|
549
|
+
@lastpage.pack_start(@msg_status, true, true, 0)
|
550
|
+
|
551
|
+
append_page(@lastpage)
|
552
|
+
set_page_title(@lastpage, "Document has not been signed")
|
553
|
+
set_page_type(@lastpage, Assistant::PAGE_SUMMARY)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
end
|