Feature #43978 » 0001-Add-ZIP-export-for-all-pages-in-a-project-s-wiki.patch
| app/controllers/wiki_controller.rb | ||
|---|---|---|
| 17 | 17 |
# along with this program; if not, write to the Free Software |
| 18 | 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
| 19 | 19 | |
| 20 |
require 'zip' |
|
| 21 | ||
| 20 | 22 |
# The WikiController follows the Rails REST controller pattern but with |
| 21 | 23 |
# a few differences |
| 22 | 24 |
# |
| ... | ... | |
| 320 | 322 |
format.pdf do |
| 321 | 323 |
send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}.pdf"
|
| 322 | 324 |
end |
| 325 |
format.zip do |
|
| 326 |
file_name = "#{@project.identifier}-wiki.zip"
|
|
| 327 |
send_data( |
|
| 328 |
wiki_pages_to_zip(@pages), |
|
| 329 |
:type => Redmine::MimeType.of(file_name), |
|
| 330 |
:filename => filename_for_content_disposition(file_name) |
|
| 331 |
) |
|
| 332 |
end |
|
| 323 | 333 |
end |
| 324 | 334 |
end |
| 325 | 335 | |
| ... | ... | |
| 400 | 410 |
includes(:parent). |
| 401 | 411 |
to_a |
| 402 | 412 |
end |
| 413 | ||
| 414 |
def wiki_pages_to_zip(pages) |
|
| 415 |
Zip.unicode_names = true |
|
| 416 |
archived_file_names = [] |
|
| 417 |
buffer = Zip::OutputStream.write_buffer do |zos| |
|
| 418 |
pages.each do |page| |
|
| 419 |
filename = archived_wiki_page_filename(page, archived_file_names) |
|
| 420 |
entry = Zip::Entry.new('', filename)
|
|
| 421 |
if page.updated_on.present? |
|
| 422 |
local_time = User.current.convert_time_to_user_timezone(page.updated_on) |
|
| 423 |
# DOS timestamp stores user's displayed local time |
|
| 424 |
entry.time = Zip::DOSTime.new( |
|
| 425 |
local_time.year, local_time.month, local_time.day, |
|
| 426 |
local_time.hour, local_time.min, local_time.sec |
|
| 427 |
) |
|
| 428 |
# UT extra field stores time in UTC |
|
| 429 |
entry.extra[:universaltime].mtime = local_time.utc |
|
| 430 |
end |
|
| 431 |
zos.put_next_entry(entry) |
|
| 432 |
zos << page.content.text.to_s |
|
| 433 |
end |
|
| 434 |
end |
|
| 435 |
buffer.string |
|
| 436 |
ensure |
|
| 437 |
buffer&.close |
|
| 438 |
end |
|
| 439 | ||
| 440 |
def archived_wiki_page_filename(page, archived_file_names) |
|
| 441 |
extension = '.txt' |
|
| 442 |
# Wiki titles are already titleized/validated to exclude spaces and the |
|
| 443 |
# characters ',', '.', '/', '?', ';', '|', ':'. We also normalize |
|
| 444 |
# '\', '*', '"', '<', '>' here so ZIP entries stay portable across |
|
| 445 |
# unzip tools and filesystems, especially on Windows. |
|
| 446 |
sanitized_title = page.title.gsub(/[\\*"<>]/, '_') |
|
| 447 |
filename = "#{sanitized_title}#{extension}"
|
|
| 448 |
dup_count = 0 |
|
| 449 | ||
| 450 |
while archived_file_names.include?(filename) |
|
| 451 |
dup_count += 1 |
|
| 452 |
filename = "#{sanitized_title}(#{dup_count})#{extension}"
|
|
| 453 |
end |
|
| 454 | ||
| 455 |
archived_file_names << filename |
|
| 456 |
filename |
|
| 457 |
end |
|
| 403 | 458 |
end |
| app/views/wiki/date_index.html.erb | ||
|---|---|---|
| 32 | 32 |
<% if User.current.allowed_to?(:export_wiki_pages, @project) %> |
| 33 | 33 |
<%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %>
|
| 34 | 34 |
<%= f.link_to('HTML', :url => {:action => 'export'}) %>
|
| 35 |
<%= f.link_to('ZIP', :url => {:action => 'export', :format => 'zip'}) %>
|
|
| 35 | 36 |
<% end %> |
| 36 | 37 |
<%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.atom_key} %>
|
| 37 | 38 |
<% end %> |
| app/views/wiki/index.html.erb | ||
|---|---|---|
| 25 | 25 |
<% if User.current.allowed_to?(:export_wiki_pages, @project) %> |
| 26 | 26 |
<%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %>
|
| 27 | 27 |
<%= f.link_to('HTML', :url => {:action => 'export'}) %>
|
| 28 |
<%= f.link_to('ZIP', :url => {:action => 'export', :format => 'zip'}) %>
|
|
| 28 | 29 |
<% end %> |
| 29 | 30 |
<%= f.link_to 'Atom', |
| 30 | 31 |
:url => {:controller => 'activities', :action => 'index',
|
| test/functional/wiki_controller_test.rb | ||
|---|---|---|
| 1146 | 1146 |
assert @response.body.starts_with?('%PDF')
|
| 1147 | 1147 |
end |
| 1148 | 1148 | |
| 1149 |
def test_export_to_zip |
|
| 1150 |
with_settings :text_formatting => 'textile' do |
|
| 1151 |
user = User.find(2) |
|
| 1152 |
user.pref.update!(:time_zone => 'Tokyo') |
|
| 1153 | ||
| 1154 |
@request.session[:user_id] = user.id |
|
| 1155 |
get :export, :params => {:project_id => 'ecookbook', :format => 'zip'}
|
|
| 1156 | ||
| 1157 |
assert_response :success |
|
| 1158 |
assert_equal 'application/zip', @response.media_type |
|
| 1159 |
assert_equal "attachment; filename=\"ecookbook-wiki.zip\"; filename*=UTF-8''ecookbook-wiki.zip", |
|
| 1160 |
@response.headers['Content-Disposition'] |
|
| 1161 | ||
| 1162 |
pages = Project.find(1).wiki.pages.includes(:content).to_a.index_by(&:title) |
|
| 1163 |
zip_entries = zip_entries_from_response |
|
| 1164 | ||
| 1165 |
assert_equal pages.keys.sort.map {|title| "#{title}.txt"}, zip_entries.keys.sort
|
|
| 1166 | ||
| 1167 |
zip_entries.each do |name, entry| |
|
| 1168 |
title = name.delete_suffix('.txt')
|
|
| 1169 |
page = pages.fetch(title) |
|
| 1170 |
local_time = user.convert_time_to_user_timezone(page.updated_on) |
|
| 1171 | ||
| 1172 |
assert_equal page.content.text, entry[:content] |
|
| 1173 |
# DOS timestamp should match the user's displayed local time. |
|
| 1174 |
assert_equal [local_time.year, local_time.month, local_time.day, local_time.hour, local_time.min, local_time.sec], |
|
| 1175 |
[entry[:time].year, entry[:time].month, entry[:time].day, entry[:time].hour, entry[:time].min, entry[:time].sec] |
|
| 1176 |
# UT extra field should store the corresponding absolute UTC time. |
|
| 1177 |
assert_equal local_time.utc.to_i, entry[:utc_time].utc.to_i |
|
| 1178 |
end |
|
| 1179 |
end |
|
| 1180 |
end |
|
| 1181 | ||
| 1182 |
def test_export_to_zip_should_sanitize_non_portable_entry_name_characters |
|
| 1183 |
with_settings :text_formatting => 'textile' do |
|
| 1184 |
page = Project.find(1).wiki.pages.new(:title => 'Foo*') |
|
| 1185 |
page.content = WikiContent.new(:text => 'sanitized') |
|
| 1186 |
assert_save page |
|
| 1187 | ||
| 1188 |
@request.session[:user_id] = 2 |
|
| 1189 |
get :export, :params => {:project_id => 'ecookbook', :format => 'zip'}
|
|
| 1190 | ||
| 1191 |
assert_response :success |
|
| 1192 | ||
| 1193 |
zip_entries = zip_entries_from_response |
|
| 1194 |
assert_equal 'sanitized', zip_entries['Foo_.txt'][:content] |
|
| 1195 |
assert_not_includes zip_entries.keys, 'Foo*.txt' |
|
| 1196 |
end |
|
| 1197 |
end |
|
| 1198 | ||
| 1149 | 1199 |
def test_export_without_permission_should_be_denied |
| 1150 | 1200 |
@request.session[:user_id] = 2 |
| 1151 | 1201 |
Role.find_by_name('Manager').remove_permission! :export_wiki_pages
|
| ... | ... | |
| 1333 | 1383 |
assert_response :success |
| 1334 | 1384 |
assert_select 'head>meta[name="robots"]', false |
| 1335 | 1385 |
end |
| 1386 | ||
| 1387 |
private |
|
| 1388 | ||
| 1389 |
def zip_entries_from_response |
|
| 1390 |
entries = {}
|
|
| 1391 | ||
| 1392 |
Zip::InputStream.open(StringIO.new(@response.body)) do |io| |
|
| 1393 |
while (entry = io.get_next_entry) |
|
| 1394 |
entries[entry.name] = {
|
|
| 1395 |
:content => io.read, |
|
| 1396 |
:time => entry.time, |
|
| 1397 |
:utc_time => entry.extra[:universaltime]&.mtime |
|
| 1398 |
} |
|
| 1399 |
end |
|
| 1400 |
end |
|
| 1401 | ||
| 1402 |
entries.transform_keys {|name| name.dup.force_encoding('UTF-8')}
|
|
| 1403 |
end |
|
| 1336 | 1404 |
end |
| test/integration/routing/wiki_test.rb | ||
|---|---|---|
| 26 | 26 |
should_route 'GET /projects/foo/wiki/date_index' => 'wiki#date_index', :project_id => 'foo' |
| 27 | 27 |
should_route 'GET /projects/foo/wiki/export' => 'wiki#export', :project_id => 'foo' |
| 28 | 28 |
should_route 'GET /projects/foo/wiki/export.pdf' => 'wiki#export', :project_id => 'foo', :format => 'pdf' |
| 29 |
should_route 'GET /projects/foo/wiki/export.zip' => 'wiki#export', :project_id => 'foo', :format => 'zip' |
|
| 29 | 30 |
end |
| 30 | 31 | |
| 31 | 32 |
def test_wiki_pages |
- « Previous
- 1
- 2
- Next »