Defect #44358 » 0001-Defect-44358-Fix-images-in-markdownized-previews.patch
| app/controllers/attachments_controller.rb | ||
|---|---|---|
| 20 | 20 |
class AttachmentsController < ApplicationController |
| 21 | 21 |
include ActionView::Helpers::NumberHelper |
| 22 | 22 | |
| 23 |
before_action :find_attachment, :only => [:show, :download, :thumbnail, :update, :destroy] |
|
| 23 |
before_action :find_attachment, :only => [:show, :download, :thumbnail, :update, :destroy, :preview_media]
|
|
| 24 | 24 |
before_action :find_container, :only => [:edit_all, :update_all, :download_all] |
| 25 | 25 |
before_action :find_downloadable_attachments, :only => :download_all |
| 26 | 26 |
before_action :find_editable_attachments, :only => [:edit_all, :update_all] |
| 27 |
before_action :file_readable, :read_authorize, :only => [:show, :download, :thumbnail] |
|
| 27 |
before_action :file_readable, :read_authorize, :only => [:show, :download, :thumbnail, :preview_media]
|
|
| 28 | 28 |
before_action :update_authorize, :only => :update |
| 29 | 29 |
before_action :delete_authorize, :only => :destroy |
| 30 | 30 |
before_action :authorize_global, :only => :upload |
| ... | ... | |
| 33 | 33 |
# MIME type text/javascript. |
| 34 | 34 |
skip_after_action :verify_same_origin_request, :only => :download |
| 35 | 35 | |
| 36 |
accept_api_auth :show, :download, :thumbnail, :upload, :update, :destroy |
|
| 36 |
accept_api_auth :show, :download, :thumbnail, :upload, :update, :destroy, :preview_media
|
|
| 37 | 37 | |
| 38 | 38 |
def show |
| 39 | 39 |
respond_to do |format| |
| ... | ... | |
| 101 | 101 |
end |
| 102 | 102 |
end |
| 103 | 103 | |
| 104 |
def preview_media |
|
| 105 |
if (media = @attachment.preview_media(params[:path])) |
|
| 106 |
if stale?(etag: media, template: false) |
|
| 107 |
send_file(media, |
|
| 108 |
:type => Redmine::MimeType.of(media).presence || 'application/octet-stream', |
|
| 109 |
:filename => "preview_#{@attachment.id}_#{File.basename(media)}",
|
|
| 110 |
:disposition => 'inline') |
|
| 111 |
end |
|
| 112 |
else |
|
| 113 |
head :not_found |
|
| 114 |
end |
|
| 115 |
end |
|
| 116 | ||
| 104 | 117 |
def upload |
| 105 | 118 |
# Make sure that API users get used to set this content type |
| 106 | 119 |
# as it won't trigger Rails' automatic parsing of the request body for parameters |
| app/models/attachment.rb | ||
|---|---|---|
| 297 | 297 |
end |
| 298 | 298 | |
| 299 | 299 |
def self.clear_markdownized_previews |
| 300 |
Dir.glob(File.join(markdownized_previews_storage_path, "*.md")).each do |file| |
|
| 301 |
File.delete file |
|
| 302 |
end |
|
| 300 |
FileUtils.rm_rf(Dir.glob(File.join(markdownized_previews_storage_path, '*'))) |
|
| 303 | 301 |
end |
| 304 | 302 | |
| 305 | 303 |
def is_text? |
| ... | ... | |
| 342 | 340 |
return nil unless markdownized_previewable? |
| 343 | 341 | |
| 344 | 342 |
target = markdownized_preview_cache_path |
| 345 |
if Redmine::Markdownizer.convert(diskfile, target) |
|
| 343 |
if Redmine::Markdownizer.convert(diskfile, target, id)
|
|
| 346 | 344 |
File.read(target, :mode => "rb") |
| 347 | 345 |
end |
| 348 | 346 |
rescue => e |
| ... | ... | |
| 355 | 353 |
nil |
| 356 | 354 |
end |
| 357 | 355 | |
| 356 |
def preview_media(path) |
|
| 357 |
base_path = File.expand_path(File.join(self.class.markdownized_previews_storage_path, id.to_s)) |
|
| 358 |
file_path = File.expand_path(path, base_path) |
|
| 359 | ||
| 360 |
return nil unless file_path.start_with?(base_path) |
|
| 361 |
return nil unless File.file?(file_path) |
|
| 362 | ||
| 363 |
file_path |
|
| 364 |
end |
|
| 365 | ||
| 366 |
def markdownized_preview_directory |
|
| 367 |
File.join(self.class.markdownized_previews_storage_path, id.to_s) |
|
| 368 |
end |
|
| 369 | ||
| 358 | 370 |
def markdownized_preview_cache_path |
| 359 |
File.join(self.class.markdownized_previews_storage_path, "#{digest}_#{filesize}.md")
|
|
| 371 |
File.join(markdownized_preview_directory, "preview.md")
|
|
| 360 | 372 |
end |
| 361 | 373 | |
| 362 | 374 |
def previewable? |
| ... | ... | |
| 590 | 602 |
Dir[thumbnail_path("*")].each do |thumb|
|
| 591 | 603 |
File.delete(thumb) |
| 592 | 604 |
end |
| 593 |
FileUtils.rm_f(markdownized_preview_cache_path)
|
|
| 605 |
FileUtils.rm_rf(markdownized_preview_directory)
|
|
| 594 | 606 |
end |
| 595 | 607 | |
| 596 | 608 |
def thumbnail_path(size) |
| config/routes.rb | ||
|---|---|---|
| 328 | 328 |
get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment', :format => 'html' |
| 329 | 329 |
get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/ |
| 330 | 330 |
get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail' |
| 331 |
get 'attachments/markdownized_previews/:id/*path', :to => 'attachments#preview_media', :id => /\d+/, :format => false |
|
| 331 | 332 |
resources :attachments, :only => [:show, :update, :destroy] |
| 332 | 333 | |
| 333 | 334 |
# register plugin object types with ObjectTypeConstraint.register_object_type(PluginModel.name.underscore.pluralize') |
| lib/redmine/markdownizer.rb | ||
|---|---|---|
| 35 | 35 |
markdownizable_extensions.include?(File.extname(filename.to_s).downcase) |
| 36 | 36 |
end |
| 37 | 37 | |
| 38 |
def self.convert(source, target) |
|
| 38 |
def self.convert(source, target, attachment_id)
|
|
| 39 | 39 |
return nil unless available? |
| 40 | 40 |
return target if File.exist?(target) |
| 41 | 41 | |
| ... | ... | |
| 45 | 45 |
end |
| 46 | 46 | |
| 47 | 47 |
directory = File.dirname(target) |
| 48 |
basedir = File.dirname(Attachment.markdownized_previews_storage_path) |
|
| 48 | 49 |
FileUtils.mkdir_p(directory) |
| 49 |
args = [COMMAND, source, "-t", "gfm"] |
|
| 50 |
args = [COMMAND, source, "-t", "gfm", "--extract-media=markdownized_previews/#{attachment_id}/"]
|
|
| 50 | 51 |
pid = nil |
| 51 | 52 |
output = Tempfile.new('markdownized-preview')
|
| 52 | 53 | |
| 53 | 54 |
begin |
| 54 | 55 |
Timeout.timeout(PREVIEW_GENERATION_TIMEOUT) do |
| 55 |
pid = Process.spawn(*args, out: output.path) |
|
| 56 |
pid = Process.spawn(*args, chdir: basedir, out: output.path)
|
|
| 56 | 57 |
_, status = Process.wait2(pid) |
| 57 | 58 |
unless status.success? |
| 58 | 59 |
logger.error("Markdownized preview generation failed (#{status.exitstatus}):\nCommand: #{args.shelljoin}")
|
| test/functional/attachments_controller_test.rb | ||
|---|---|---|
| 287 | 287 |
assert_equal 'text/html', @response.media_type |
| 288 | 288 |
assert_select 'div.filecontent.wiki', :text => /Redmine is a flexible project management web application/ |
| 289 | 289 |
assert_select '.nodata', :count => 0 |
| 290 |
assert_select 'div.filecontent.wiki img', 1 |
|
| 291 |
img = css_select('div.filecontent.wiki img').first
|
|
| 292 |
assert_match(%r{markdownized_previews/#{a.id}/}, img['src'])
|
|
| 293 |
path = img['src'][%r{markdownized_previews/#{a.id}/(.+)$}, 1]
|
|
| 294 | ||
| 295 |
get(:preview_media, :params => {:id => a.id, :path => path})
|
|
| 296 | ||
| 297 |
assert_response :success |
|
| 298 |
assert_equal 'image/png', response.media_type |
|
| 290 | 299 |
end |
| 291 | 300 | |
| 292 | 301 |
def test_show_libreoffice_writer |
| ... | ... | |
| 309 | 318 |
assert_equal 'text/html', @response.media_type |
| 310 | 319 |
assert_select 'div.filecontent.wiki', :text => /Redmine is a flexible project management web application/ |
| 311 | 320 |
assert_select '.nodata', :count => 0 |
| 321 |
assert_select 'div.filecontent.wiki img', 1 |
|
| 322 |
img = css_select('div.filecontent.wiki img').first
|
|
| 323 |
assert_match(%r{markdownized_previews/#{a.id}/}, img['src'])
|
|
| 324 |
path = img['src'][%r{markdownized_previews/#{a.id}/(.+)$}, 1]
|
|
| 325 | ||
| 326 |
get(:preview_media, :params => {:id => a.id, :path => path})
|
|
| 327 | ||
| 328 |
assert_response :success |
|
| 329 |
assert_equal 'image/png', response.media_type |
|
| 330 |
end |
|
| 331 | ||
| 332 |
def test_preview_media_from_private_issue_without_permission |
|
| 333 |
attachment = Attachment.find(15) |
|
| 334 |
FileUtils.mkdir_p(File.join(attachment.markdownized_preview_directory, 'Pictures')) |
|
| 335 |
File.binwrite(File.join(attachment.markdownized_preview_directory, 'Pictures', 'image.png'), 'PNG') |
|
| 336 | ||
| 337 |
get(:preview_media, :params => {:id => attachment.id, :path => 'Pictures/image.png'})
|
|
| 338 | ||
| 339 |
assert_response :unauthorized |
|
| 340 |
end |
|
| 341 | ||
| 342 |
def test_preview_media_from_private_issue_with_permission |
|
| 343 |
@request.session[:user_id] = 2 |
|
| 344 | ||
| 345 |
attachment = Attachment.find(15) |
|
| 346 |
FileUtils.mkdir_p(File.join(attachment.markdownized_preview_directory, 'Pictures')) |
|
| 347 |
File.binwrite(File.join(attachment.markdownized_preview_directory, 'Pictures', 'image.png'), 'PNG') |
|
| 348 | ||
| 349 |
get(:preview_media, :params => {:id => attachment.id, :path => 'Pictures/image.png'})
|
|
| 350 | ||
| 351 |
assert_response :success |
|
| 352 |
assert_equal 'image/png', response.media_type |
|
| 353 |
end |
|
| 354 | ||
| 355 |
def test_preview_media_should_be_denied_without_permission |
|
| 356 |
@request.session[:user_id] = 3 |
|
| 357 | ||
| 358 |
attachment = Attachment.find(15) |
|
| 359 |
FileUtils.mkdir_p(File.join(attachment.markdownized_preview_directory, 'Pictures')) |
|
| 360 |
File.binwrite(File.join(attachment.markdownized_preview_directory, 'Pictures', 'image.png'), 'PNG') |
|
| 361 | ||
| 362 |
get(:preview_media, :params => {:id => attachment.id, :path => 'Pictures/image.png'})
|
|
| 363 | ||
| 364 |
assert_response :forbidden |
|
| 365 |
end |
|
| 366 | ||
| 367 |
def test_preview_media_should_return_404_for_missing_file |
|
| 368 |
@request.session[:user_id] = 2 |
|
| 369 | ||
| 370 |
get(:preview_media, :params => {:id => 16, :path => 'media/missing.png'})
|
|
| 371 | ||
| 372 |
assert_response :not_found |
|
| 373 |
end |
|
| 374 | ||
| 375 |
def test_preview_media_should_return_404_for_invalid_path |
|
| 376 |
@request.session[:user_id] = 2 |
|
| 377 | ||
| 378 |
get(:preview_media, :params => {:id => 16, :path => '../secret.txt'})
|
|
| 379 | ||
| 380 |
assert_response :not_found |
|
| 312 | 381 |
end |
| 313 | 382 | |
| 314 | 383 |
def test_show_other_with_no_preview |
| test/integration/routing/attachments_test.rb | ||
|---|---|---|
| 31 | 31 |
should_route 'GET /attachments/thumbnail/1' => 'attachments#thumbnail', :id => '1' |
| 32 | 32 |
should_route 'GET /attachments/thumbnail/1/200' => 'attachments#thumbnail', :id => '1', :size => '200' |
| 33 | 33 | |
| 34 |
should_route 'GET /attachments/markdownized_previews/1/pictures/image.png' => 'attachments#preview_media', :id => '1', :path => 'pictures/image.png' |
|
| 35 | ||
| 34 | 36 |
should_route 'DELETE /attachments/1' => 'attachments#destroy', :id => '1' |
| 35 | 37 | |
| 36 | 38 |
should_route 'GET /attachments/issues/1/edit' => 'attachments#edit_all', :object_type => 'issues', :object_id => '1' |
| test/unit/attachment_test.rb | ||
|---|---|---|
| 683 | 683 |
), |
| 684 | 684 |
:author => User.find(1) |
| 685 | 685 |
) |
| 686 |
preview = attachment.markdownized_preview_cache_path |
|
| 687 |
FileUtils.mkdir_p(File.dirname(preview)) |
|
| 688 |
File.write(preview, "preview") |
|
| 689 |
assert File.exist?(preview) |
|
| 686 |
preview_dir = attachment.markdownized_preview_directory |
|
| 687 |
attachment.markdownized_preview_content |
|
| 688 |
assert Dir.exist?(preview_dir) |
|
| 690 | 689 | |
| 691 | 690 |
attachment.send(:delete_from_disk!) |
| 692 |
assert_not File.exist?(preview)
|
|
| 691 |
assert_not Dir.exist?(preview_dir)
|
|
| 693 | 692 |
end |
| 694 | 693 | |
| 695 | 694 |
if convert_installed? |