]> git.uio.no Git - usit-rt.git/blobdiff - lib/RT/Attachment.pm
Upgrade to 4.2.2
[usit-rt.git] / lib / RT / Attachment.pm
index 54217b32b58df768da74d5cd127763895d8da655..800f4f5b48e7ab941ef1c172c000331ab9dba5be 100644 (file)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -127,28 +127,40 @@ sub Create {
     # If we possibly can, collapse it to a singlepart
     $Attachment->make_singlepart;
 
+    my $head = $Attachment->head;
+
     # Get the subject
-    my $Subject = $Attachment->head->get( 'subject', 0 );
+    my $Subject = $head->get( 'subject', 0 );
     $Subject = '' unless defined $Subject;
     chomp $Subject;
     utf8::decode( $Subject ) unless utf8::is_utf8( $Subject );
 
     #Get the Message-ID
-    my $MessageId = $Attachment->head->get( 'Message-ID', 0 );
+    my $MessageId = $head->get( 'Message-ID', 0 );
     defined($MessageId) or $MessageId = '';
     chomp ($MessageId);
     $MessageId =~ s/^<(.*?)>$/$1/o;
 
     #Get the filename
-
     my $Filename = mime_recommended_filename($Attachment);
 
     # remove path part. 
     $Filename =~ s!.*/!! if $Filename;
 
+    my $content;
+    unless ( $head->get('Content-Length') ) {
+        my $length = 0;
+        if ( defined $Attachment->bodyhandle ) {
+            $content = $Attachment->bodyhandle->as_string;
+            utf8::encode( $content ) if utf8::is_utf8( $content );
+            $length = length $content;
+        }
+        $head->replace( 'Content-Length' => $length );
+    }
+    $head = $head->as_string;
+
     # MIME::Head doesn't support perl strings well and can return
     # octets which later will be double encoded in low-level code
-    my $head = $Attachment->head->as_string;
     utf8::decode( $head ) unless utf8::is_utf8( $head );
 
     # If a message has no bodyhandle, that means that it has subparts (or appears to)
@@ -165,6 +177,7 @@ sub Create {
 
         unless ($id) {
             $RT::Logger->crit("Attachment insert failed - ". $RT::Handle->dbh->errstr);
+            return ($id);
         }
 
         foreach my $part ( $Attachment->parts ) {
@@ -176,6 +189,7 @@ sub Create {
             );
             unless ($id) {
                 $RT::Logger->crit("Attachment insert failed: ". $RT::Handle->dbh->errstr);
+                return ($id);
             }
         }
         return ($id);
@@ -184,7 +198,8 @@ sub Create {
     #If it's not multipart
     else {
 
-        my ($ContentEncoding, $Body, $ContentType, $Filename) = $self->_EncodeLOB(
+        my ($encoding, $type);
+        ($encoding, $content, $type, $Filename) = $self->_EncodeLOB(
             $Attachment->bodyhandle->as_string,
             $Attachment->mime_type,
             $Filename
@@ -192,12 +207,12 @@ sub Create {
 
         my $id = $self->SUPER::Create(
             TransactionId   => $args{'TransactionId'},
-            ContentType     => $ContentType,
-            ContentEncoding => $ContentEncoding,
+            ContentType     => $type,
+            ContentEncoding => $encoding,
             Parent          => $args{'Parent'},
             Headers         => $head,
             Subject         => $Subject,
-            Content         => $Body,
+            Content         => $content,
             Filename        => $Filename,
             MessageId       => $MessageId,
         );
@@ -209,22 +224,6 @@ sub Create {
     }
 }
 
-=head2 Import
-
-Create an attachment exactly as specified in the named parameters.
-
-=cut
-
-sub Import {
-    my $self = shift;
-    my %args = ( ContentEncoding => 'none', @_ );
-
-    ( $args{'ContentEncoding'}, $args{'Content'} ) =
-        $self->_EncodeLOB( $args{'Content'}, $args{'MimeType'} );
-
-    return ( $self->SUPER::Create(%args) );
-}
-
 =head2 TransactionObj
 
 Returns the transaction object asscoiated with this attachment.
@@ -263,6 +262,35 @@ sub ParentObj {
     return $parent;
 }
 
+=head2 Closest
+
+Takes a MIME type as a string or regex.  Returns an L<RT::Attachment> object
+for the nearest containing part with a matching L</ContentType>.  Strings must
+match exactly and all matches are done case insensitively.  Strings ending in a
+C</> must only match the first part of the MIME type.  For example:
+
+    # Find the nearest multipart/* container
+    my $container = $attachment->Closest("multipart/");
+
+Returns undef if no such object is found.
+
+=cut
+
+sub Closest {
+    my $self = shift;
+    my $type = shift;
+    my $part = $self->ParentObj or return undef;
+
+    $type = qr/^\Q$type\E$/
+        unless ref $type eq "REGEX";
+
+    while (lc($part->ContentType) !~ $type) {
+        $part = $part->ParentObj or last;
+    }
+
+    return ($part and $part->id) ? $part : undef;
+}
+
 =head2 Children
 
 Returns an L<RT::Attachments> object which is preloaded with
@@ -279,6 +307,30 @@ sub Children {
     return($kids);
 }
 
+=head2 Siblings
+
+Returns an L<RT::Attachments> object containing all the attachments sharing
+the same immediate parent as the current object, excluding the current
+attachment itself.
+
+If the current attachment is a top-level part (i.e. Parent == 0) then a
+guaranteed empty L<RT::Attachments> object is returned.
+
+=cut
+
+sub Siblings {
+    my $self = shift;
+    my $siblings = RT::Attachments->new( $self->CurrentUser );
+    if ($self->Parent) {
+        $siblings->ChildrenOf( $self->Parent );
+        $siblings->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->Id );
+    } else {
+        # Ensure emptiness
+        $siblings->Limit( SUBCLAUSE => 'empty', FIELD => 'id', VALUE => 0 );
+    }
+    return $siblings;
+}
+
 =head2 Content
 
 Returns the attachment's content. if it's base64 encoded, decode it 
@@ -381,59 +433,32 @@ sub ContentLength {
     return $len;
 }
 
-=head2 Quote
-
-=cut
+=head2 FriendlyContentLength
 
-sub Quote {
-    my $self=shift;
-    my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
-             @_);
+Returns L</ContentLength> in bytes, kilobytes, or megabytes as most
+appropriate.  The size is suffixed with C<M>, C<k>, and C<b> and the returned
+string is localized.
 
-    my ($quoted_content, $body, $headers);
-    my $max=0;
+Returns the empty string if the L</ContentLength> is 0 or undefined.
 
-    # TODO: Handle Multipart/Mixed (eventually fix the link in the
-    # ShowHistory web template?)
-    if (RT::I18N::IsTextualContentType($self->ContentType)) {
-       $body=$self->Content;
-
-       # Do we need any preformatting (wrapping, that is) of the message?
-
-       # Remove quoted signature.
-       $body =~ s/\n-- \n(.*)$//s;
-
-       # What's the longest line like?
-       foreach (split (/\n/,$body)) {
-           $max=length if ( length > $max);
-       }
-
-       if ($max>76) {
-           require Text::Wrapper;
-           my $wrapper = Text::Wrapper->new
-               (
-                columns => 70, 
-                body_start => ($max > 70*3 ? '   ' : ''),
-                par_start => ''
-                );
-           $body=$wrapper->wrap($body);
-       }
-
-       $body =~ s/^/> /gm;
+=cut
 
-       $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
-                   . "]:\n\n"
-               . $body . "\n\n";
+sub FriendlyContentLength {
+    my $self = shift;
+    my $size = $self->ContentLength;
+    return '' unless $size;
 
-    } else {
-       $body = "[Non-text message not quoted]\n\n";
+    my $res = '';
+    if ( $size > 1024*1024 ) {
+        $res = $self->loc( "[_1]M", int( $size / 1024 / 102.4 ) / 10 );
     }
-    
-    $max=60 if $max<60;
-    $max=70 if $max>78;
-    $max+=2;
-
-    return (\$body, $max);
+    elsif ( $size > 1024 ) {
+        $res = $self->loc( "[_1]k", int( $size / 102.4 ) / 10 );
+    }
+    else {
+        $res = $self->loc( "[_1]b", $size );
+    }
+    return $res;
 }
 
 =head2 ContentAsMIME [Children => 1]
@@ -533,9 +558,9 @@ sub NiceHeaders {
     my $hdrs = "";
     my @hdrs = $self->_SplitHeaders;
     while (my $str = shift @hdrs) {
-           next unless $str =~ /^(To|From|RT-Send-Cc|Cc|Bcc|Date|Subject):/i;
-           $hdrs .= $str . "\n";
-           $hdrs .= shift( @hdrs ) . "\n" while ($hdrs[0] =~ /^[ \t]+/);
+        next unless $str =~ /^(To|From|RT-Send-Cc|Cc|Bcc|Date|Subject):/i;
+        $hdrs .= $str . "\n";
+        $hdrs .= shift( @hdrs ) . "\n" while ($hdrs[0] =~ /^[ \t]+/);
     }
     return $hdrs;
 }
@@ -712,20 +737,16 @@ sub Encrypt {
     return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
     return (0, $self->loc('Permission Denied'))
         unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
-    return (0, $self->loc('GnuPG integration is disabled'))
-        unless RT->Config->Get('GnuPG')->{'Enable'};
+    return (0, $self->loc('Cryptography is disabled'))
+        unless RT->Config->Get('Crypt')->{'Enable'};
     return (0, $self->loc('Attachments encryption is disabled'))
-        unless RT->Config->Get('GnuPG')->{'AllowEncryptDataInDB'};
-
-    require RT::Crypt::GnuPG;
+        unless RT->Config->Get('Crypt')->{'AllowEncryptDataInDB'};
 
     my $type = $self->ContentType;
-    if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
+    if ( $type =~ /^x-application-rt\/[^-]+-encrypted/i ) {
         return (1, $self->loc('Already encrypted'));
     } elsif ( $type =~ /^multipart\//i ) {
         return (1, $self->loc('No need to encrypt'));
-    } else {
-        $type = qq{x-application-rt\/gpg-encrypted; original-type="$type"};
     }
 
     my $queue = $txn->TicketObj->QueueObj;
@@ -736,9 +757,9 @@ sub Encrypt {
         RT->Config->Get('CorrespondAddress'),
         RT->Config->Get('CommentAddress'),
     ) {
-        my %res = RT::Crypt::GnuPG::GetKeysInfo( $address, 'private' );
+        my %res = RT::Crypt->GetKeysInfo( Key => $address, Type => 'private' );
         next if $res{'exit_code'} || !$res{'info'};
-        %res = RT::Crypt::GnuPG::GetKeysForEncryption( $address );
+        %res = RT::Crypt->GetKeysForEncryption( $address );
         next if $res{'exit_code'} || !$res{'info'};
         $encrypt_for = $address;
     }
@@ -746,24 +767,26 @@ sub Encrypt {
         return (0, $self->loc('No key suitable for encryption'));
     }
 
-    $self->__Set( Field => 'ContentType', Value => $type );
-    $self->SetHeader( 'Content-Type' => $type );
-
     my $content = $self->Content;
-    my %res = RT::Crypt::GnuPG::SignEncryptContent(
+    my %res = RT::Crypt->SignEncryptContent(
         Content => \$content,
         Sign => 0,
         Encrypt => 1,
         Recipients => [ $encrypt_for ],
     );
     if ( $res{'exit_code'} ) {
-        return (0, $self->loc('GnuPG error. Contact with administrator'));
+        return (0, $self->loc('Encryption error; contact the administrator'));
     }
 
     my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
     unless ( $status ) {
         return ($status, $self->loc("Couldn't replace content with encrypted data: [_1]", $msg));
     }
+
+    $type = qq{x-application-rt\/$res{'Protocol'}-encrypted; original-type="$type"};
+    $self->__Set( Field => 'ContentType', Value => $type );
+    $self->SetHeader( 'Content-Type' => $type );
+
     return (1, $self->loc('Successfuly encrypted data'));
 }
 
@@ -774,31 +797,45 @@ sub Decrypt {
     return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
     return (0, $self->loc('Permission Denied'))
         unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
-    return (0, $self->loc('GnuPG integration is disabled'))
-        unless RT->Config->Get('GnuPG')->{'Enable'};
-
-    require RT::Crypt::GnuPG;
+    return (0, $self->loc('Cryptography is disabled'))
+        unless RT->Config->Get('Crypt')->{'Enable'};
 
     my $type = $self->ContentType;
-    if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
+    my $protocol;
+    if ( $type =~ /^x-application-rt\/([^-]+)-encrypted/i ) {
+        $protocol = $1;
+        $protocol =~ s/gpg/gnupg/; # backwards compatibility
         ($type) = ($type =~ /original-type="(.*)"/i);
         $type ||= 'application/octet-stream';
     } else {
         return (1, $self->loc('Is not encrypted'));
     }
-    $self->__Set( Field => 'ContentType', Value => $type );
-    $self->SetHeader( 'Content-Type' => $type );
+
+    my $queue = $txn->TicketObj->QueueObj;
+    my @addresses =
+        $queue->CorrespondAddress,
+        $queue->CommentAddress,
+        RT->Config->Get('CorrespondAddress'),
+        RT->Config->Get('CommentAddress')
+    ;
 
     my $content = $self->Content;
-    my %res = RT::Crypt::GnuPG::DecryptContent( Content => \$content, );
+    my %res = RT::Crypt->DecryptContent(
+        Protocol => $protocol,
+        Content => \$content,
+        Recipients => \@addresses,
+    );
     if ( $res{'exit_code'} ) {
-        return (0, $self->loc('GnuPG error. Contact with administrator'));
+        return (0, $self->loc('Decryption error; contact the administrator'));
     }
 
     my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
     unless ( $status ) {
         return ($status, $self->loc("Couldn't replace content with decrypted data: [_1]", $msg));
     }
+    $self->__Set( Field => 'ContentType', Value => $type );
+    $self->SetHeader( 'Content-Type' => $type );
+
     return (1, $self->loc('Successfuly decrypted data'));
 }
 
@@ -1029,33 +1066,41 @@ sub _CoreAccessible {
     {
 
         id =>
-               {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
+                {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
         TransactionId =>
-               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
+                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
         Parent =>
-               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         MessageId =>
-               {read => 1, write => 1, sql_type => 12, length => 160,  is_blob => 0,  is_numeric => 0,  type => 'varchar(160)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 160,  is_blob => 0,  is_numeric => 0,  type => 'varchar(160)', default => ''},
         Subject =>
-               {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         Filename =>
-               {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         ContentType =>
-               {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
         ContentEncoding =>
-               {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
         Content =>
-               {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'longblob', default => ''},
+                {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'longblob', default => ''},
         Headers =>
-               {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'longtext', default => ''},
+                {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'longtext', default => ''},
         Creator =>
-               {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+                {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         Created =>
-               {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+                {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
 
  }
 };
 
+sub FindDependencies {
+    my $self = shift;
+    my ($walker, $deps) = @_;
+
+    $self->SUPER::FindDependencies($walker, $deps);
+    $deps->Add( out => $self->TransactionObj );
+}
+
 RT::Base->_ImportOverlays();
 
 1;