| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| # |
| # This Source Code Form is "Incompatible With Secondary Licenses", as |
| # defined by the Mozilla Public License, v. 2.0. |
| |
| package Bugzilla::Attachment::PatchReader; |
| |
| use 5.10.1; |
| use strict; |
| use warnings; |
| |
| use Config; |
| use IO::Select; |
| use IPC::Open3; |
| use Symbol 'gensym'; |
| |
| use Bugzilla::Error; |
| use Bugzilla::Attachment; |
| use Bugzilla::Util; |
| |
| use constant PERLIO_IS_ENABLED => $Config{useperlio}; |
| |
| sub process_diff { |
| my ($attachment, $format) = @_; |
| my $dbh = Bugzilla->dbh; |
| my $cgi = Bugzilla->cgi; |
| my $lc = Bugzilla->localconfig; |
| my $vars = {}; |
| |
| require PatchReader::Raw; |
| my $reader = new PatchReader::Raw; |
| |
| if ($format eq 'raw') { |
| require PatchReader::DiffPrinter::raw; |
| $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); |
| # Actually print out the patch. |
| print $cgi->header(-type => 'text/plain'); |
| disable_utf8(); |
| $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); |
| } |
| else { |
| my @other_patches = (); |
| if ($lc->{interdiffbin} && $lc->{diffpath}) { |
| # Get the list of attachments that the user can view in this bug. |
| my @attachments = |
| @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug)}; |
| # Extract patches only. |
| @attachments = grep {$_->ispatch == 1} @attachments; |
| # We want them sorted from newer to older. |
| @attachments = sort { $b->id <=> $a->id } @attachments; |
| |
| # Ignore the current patch, but select the one right before it |
| # chronologically. |
| my $select_next_patch = 0; |
| foreach my $attach (@attachments) { |
| if ($attach->id == $attachment->id) { |
| $select_next_patch = 1; |
| } |
| else { |
| push(@other_patches, { 'id' => $attach->id, |
| 'desc' => $attach->description, |
| 'selected' => $select_next_patch }); |
| $select_next_patch = 0; |
| } |
| } |
| } |
| |
| $vars->{'bugid'} = $attachment->bug_id; |
| $vars->{'attachid'} = $attachment->id; |
| $vars->{'description'} = $attachment->description; |
| $vars->{'other_patches'} = \@other_patches; |
| |
| setup_template_patch_reader($reader, $vars); |
| # The patch is going to be displayed in a HTML page and if the utf8 |
| # param is enabled, we have to encode attachment data as utf8. |
| if (Bugzilla->params->{'utf8'}) { |
| $attachment->data; # Populate ->{data} |
| utf8::decode($attachment->{data}); |
| } |
| $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); |
| } |
| } |
| |
| sub process_interdiff { |
| my ($old_attachment, $new_attachment, $format) = @_; |
| my $cgi = Bugzilla->cgi; |
| my $lc = Bugzilla->localconfig; |
| my $vars = {}; |
| |
| require PatchReader::Raw; |
| |
| # Encode attachment data as utf8 if it's going to be displayed in a HTML |
| # page using the UTF-8 encoding. |
| if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { |
| $old_attachment->data; # Populate ->{data} |
| utf8::decode($old_attachment->{data}); |
| $new_attachment->data; # Populate ->{data} |
| utf8::decode($new_attachment->{data}); |
| } |
| |
| # Get old patch data. |
| my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format); |
| # Get new patch data. |
| my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format); |
| |
| my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); |
| |
| # Send through interdiff, send output directly to template. |
| # Must hack path so that interdiff will work. |
| local $ENV{'PATH'} = $lc->{diffpath}; |
| |
| # Open the interdiff pipe, reading from both STDOUT and STDERR |
| # To avoid deadlocks, we have to read the entire output from all handles |
| my ($stdout, $stderr) = ('', ''); |
| my ($pid, $interdiff_stdout, $interdiff_stderr, $use_select); |
| if ($ENV{MOD_PERL}) { |
| require Apache2::RequestUtil; |
| require Apache2::SubProcess; |
| my $request = Apache2::RequestUtil->request; |
| (undef, $interdiff_stdout, $interdiff_stderr) = $request->spawn_proc_prog( |
| $lc->{interdiffbin}, [$old_filename, $new_filename] |
| ); |
| $use_select = !PERLIO_IS_ENABLED; |
| } else { |
| $interdiff_stderr = gensym; |
| $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, |
| $lc->{interdiffbin}, $old_filename, $new_filename); |
| $use_select = 1; |
| } |
| |
| if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { |
| binmode $interdiff_stdout, ':utf8'; |
| binmode $interdiff_stderr, ':utf8'; |
| } else { |
| binmode $interdiff_stdout; |
| binmode $interdiff_stderr; |
| } |
| |
| if ($use_select) { |
| my $select = IO::Select->new(); |
| $select->add($interdiff_stdout, $interdiff_stderr); |
| while (my @handles = $select->can_read) { |
| foreach my $handle (@handles) { |
| my $line = <$handle>; |
| if (!defined $line) { |
| $select->remove($handle); |
| next; |
| } |
| if ($handle == $interdiff_stdout) { |
| $stdout .= $line; |
| } else { |
| $stderr .= $line; |
| } |
| } |
| } |
| waitpid($pid, 0) if $pid; |
| |
| } else { |
| local $/ = undef; |
| $stdout = <$interdiff_stdout>; |
| $stdout //= ''; |
| $stderr = <$interdiff_stderr>; |
| $stderr //= ''; |
| } |
| |
| close($interdiff_stdout), |
| close($interdiff_stderr); |
| |
| # Tidy up |
| unlink($old_filename) or warn "Could not unlink $old_filename: $!"; |
| unlink($new_filename) or warn "Could not unlink $new_filename: $!"; |
| |
| # Any output on STDERR means interdiff failed to full process the patches. |
| # Interdiff's error messages are generic and not useful to end users, so we |
| # show a generic failure message. |
| if ($stderr) { |
| warn($stderr); |
| $warning = 'interdiff3'; |
| } |
| |
| my $reader = new PatchReader::Raw; |
| |
| if ($format eq 'raw') { |
| require PatchReader::DiffPrinter::raw; |
| $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); |
| # Actually print out the patch. |
| print $cgi->header(-type => 'text/plain'); |
| disable_utf8(); |
| } |
| else { |
| $vars->{'warning'} = $warning if $warning; |
| $vars->{'bugid'} = $new_attachment->bug_id; |
| $vars->{'oldid'} = $old_attachment->id; |
| $vars->{'old_desc'} = $old_attachment->description; |
| $vars->{'newid'} = $new_attachment->id; |
| $vars->{'new_desc'} = $new_attachment->description; |
| |
| setup_template_patch_reader($reader, $vars); |
| } |
| $reader->iterate_string('interdiff #' . $old_attachment->id . |
| ' #' . $new_attachment->id, $stdout); |
| } |
| |
| ###################### |
| # Internal routines |
| ###################### |
| |
| sub get_unified_diff { |
| my ($attachment, $format) = @_; |
| |
| # Bring in the modules we need. |
| require PatchReader::Raw; |
| require PatchReader::DiffPrinter::raw; |
| require PatchReader::PatchInfoGrabber; |
| require File::Temp; |
| |
| $attachment->ispatch |
| || ThrowCodeError('must_be_patch', { 'attach_id' => $attachment->id }); |
| |
| # Reads in the patch, converting to unified diff in a temp file. |
| my $reader = new PatchReader::Raw; |
| my $last_reader = $reader; |
| |
| # Grabs the patch file info. |
| my $patch_info_grabber = new PatchReader::PatchInfoGrabber(); |
| $last_reader->sends_data_to($patch_info_grabber); |
| $last_reader = $patch_info_grabber; |
| |
| # Prints out to temporary file. |
| my ($fh, $filename) = File::Temp::tempfile(); |
| if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { |
| # The HTML page will be displayed with the UTF-8 encoding. |
| binmode $fh, ':utf8'; |
| } |
| my $raw_printer = new PatchReader::DiffPrinter::raw($fh); |
| $last_reader->sends_data_to($raw_printer); |
| $last_reader = $raw_printer; |
| |
| # Iterate! |
| $reader->iterate_string($attachment->id, $attachment->data); |
| |
| return ($filename, $patch_info_grabber->patch_info()->{files}); |
| } |
| |
| sub warn_if_interdiff_might_fail { |
| my ($old_file_list, $new_file_list) = @_; |
| |
| # Verify that the list of files diffed is the same. |
| my @old_files = sort keys %{$old_file_list}; |
| my @new_files = sort keys %{$new_file_list}; |
| if (@old_files != @new_files |
| || join(' ', @old_files) ne join(' ', @new_files)) |
| { |
| return 'interdiff1'; |
| } |
| |
| # Verify that the revisions in the files are the same. |
| foreach my $file (keys %{$old_file_list}) { |
| if (exists $old_file_list->{$file}{old_revision} |
| && exists $new_file_list->{$file}{old_revision} |
| && $old_file_list->{$file}{old_revision} ne |
| $new_file_list->{$file}{old_revision}) |
| { |
| return 'interdiff2'; |
| } |
| } |
| return undef; |
| } |
| |
| sub setup_template_patch_reader { |
| my ($last_reader, $vars) = @_; |
| my $cgi = Bugzilla->cgi; |
| my $template = Bugzilla->template; |
| |
| require PatchReader::DiffPrinter::template; |
| |
| # Define the vars for templates. |
| if (defined $cgi->param('headers')) { |
| $vars->{'headers'} = $cgi->param('headers'); |
| } |
| else { |
| $vars->{'headers'} = 1; |
| } |
| |
| $vars->{'collapsed'} = $cgi->param('collapsed'); |
| |
| # Print everything out. |
| print $cgi->header(-type => 'text/html'); |
| |
| $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template, |
| 'attachment/diff-header.html.tmpl', |
| 'attachment/diff-file.html.tmpl', |
| 'attachment/diff-footer.html.tmpl', |
| $vars)); |
| } |
| |
| 1; |
| |
| __END__ |
| |
| =head1 B<Methods in need of POD> |
| |
| =over |
| |
| =item get_unified_diff |
| |
| =item process_diff |
| |
| =item warn_if_interdiff_might_fail |
| |
| =item setup_template_patch_reader |
| |
| =item process_interdiff |
| |
| =back |