import of rt 3.0.4
[freeside.git] / rt / lib / RT / Attachment_Overlay.pm
1 # BEGIN LICENSE BLOCK
2
3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
4
5 # (Except where explictly superceded by other copyright notices)
6
7 # This work is made available to you under the terms of Version 2 of
8 # the GNU General Public License. A copy of that license should have
9 # been provided with this software, but in any event can be snarfed
10 # from www.gnu.org.
11
12 # This work is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 # General Public License for more details.
16
17 # Unless otherwise specified, all modifications, corrections or
18 # extensions to this work which alter its source code become the
19 # property of Best Practical Solutions, LLC when submitted for
20 # inclusion in the work.
21
22
23 # END LICENSE BLOCK
24 =head1 SYNOPSIS
25
26   use RT::Attachment;
27
28
29 =head1 DESCRIPTION
30
31 This module should never be instantiated directly by client code. it's an internal 
32 module which should only be instantiated through exported APIs in Ticket, Queue and other 
33 similar objects.
34
35
36 =head1 METHODS
37
38
39 =begin testing
40
41 ok (require RT::Attachment);
42
43 =end testing
44
45 =cut
46
47 use strict;
48 no warnings qw(redefine);
49
50 use MIME::Base64;
51 use MIME::QuotedPrint;
52
53 # {{{ sub _ClassAccessible 
54 sub _ClassAccessible {
55     {
56     TransactionId   => { 'read'=>1, 'public'=>1, },
57     MessageId       => { 'read'=>1, },
58     Parent          => { 'read'=>1, },
59     ContentType     => { 'read'=>1, },
60     Subject         => { 'read'=>1, },
61     Content         => { 'read'=>1, },
62     ContentEncoding => { 'read'=>1, },
63     Headers         => { 'read'=>1, },
64     Filename        => { 'read'=>1, },
65     Creator         => { 'read'=>1, 'auto'=>1, },
66     Created         => { 'read'=>1, 'auto'=>1, },
67   };
68 }
69 # }}}
70
71 # {{{ sub TransactionObj 
72
73 =head2 TransactionObj
74
75 Returns the transaction object asscoiated with this attachment.
76
77 =cut
78
79 sub TransactionObj {
80     require RT::Transaction;
81     my $self=shift;
82     unless (exists $self->{_TransactionObj}) {
83         $self->{_TransactionObj}=RT::Transaction->new($self->CurrentUser);
84         $self->{_TransactionObj}->Load($self->TransactionId);
85     }
86     return $self->{_TransactionObj};
87 }
88
89 # }}}
90
91 # {{{ sub Create 
92
93 =head2 Create
94
95 Create a new attachment. Takes a paramhash:
96     
97     'Attachment' Should be a single MIME body with optional subparts
98     'Parent' is an optional Parent RT::Attachment object
99     'TransactionId' is the mandatory id of the Transaction this attachment is associated with.;
100
101 =cut
102
103 sub Create {
104     my $self = shift;
105     my ($id);
106     my %args = ( id            => 0,
107                  TransactionId => 0,
108                  Parent        => 0,
109                  Attachment    => undef,
110                  @_ );
111
112     #For ease of reference
113     my $Attachment = $args{'Attachment'};
114
115     #if we didn't specify a ticket, we need to bail
116     if ( $args{'TransactionId'} == 0 ) {
117         $RT::Logger->crit( "RT::Attachment->Create couldn't, as you didn't specify a transaction\n" );
118         return (0);
119
120     }
121
122     #If we possibly can, collapse it to a singlepart
123     $Attachment->make_singlepart;
124
125     #Get the subject
126     my $Subject = $Attachment->head->get( 'subject', 0 );
127     defined($Subject) or $Subject = '';
128     chomp($Subject);
129
130     #Get the filename
131     my $Filename = $Attachment->head->recommended_filename || eval {
132         ${ $Attachment->head->{mail_hdr_hash}{'Content-Disposition'}[0] }
133             =~ /^.*\bfilename="(.*)"$/ ? $1 : ''
134     };
135
136     if ( $Attachment->parts ) {
137         $id = $self->SUPER::Create(
138             TransactionId => $args{'TransactionId'},
139             Parent        => 0,
140             ContentType   => $Attachment->mime_type,
141             Headers => $Attachment->head->as_string,
142             Subject => $Subject);
143
144         foreach my $part ( $Attachment->parts ) {
145             my $SubAttachment = new RT::Attachment( $self->CurrentUser );
146             $SubAttachment->Create(
147                 TransactionId => $args{'TransactionId'},
148                 Parent        => $id,
149                 Attachment    => $part,
150                 ContentType   => $Attachment->mime_type,
151                 Headers       => $Attachment->head->as_string(),
152
153             );
154         }
155         return ($id);
156     }
157
158     #If it's not multipart
159     else {
160
161         my $ContentEncoding = 'none';
162
163         my $Body = $Attachment->bodyhandle->as_string;
164
165         #get the max attachment length from RT
166         my $MaxSize = $RT::MaxAttachmentSize;
167
168         #if the current attachment contains nulls and the 
169         #database doesn't support embedded nulls
170
171         if ( $RT::AlwaysUseBase64 or
172              ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
173
174             # set a flag telling us to mimencode the attachment
175             $ContentEncoding = 'base64';
176
177             #cut the max attchment size by 25% (for mime-encoding overhead.
178             $RT::Logger->debug("Max size is $MaxSize\n");
179             $MaxSize = $MaxSize * 3 / 4;
180         # Some databases (postgres) can't handle non-utf8 data 
181         } elsif (    !$RT::Handle->BinarySafeBLOBs
182                   && $Attachment->mime_type !~ /text\/plain/gi
183                   && !Encode::is_utf8( $Body, 1 ) ) {
184               $ContentEncoding = 'quoted-printable';
185         }
186
187         #if the attachment is larger than the maximum size
188         if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
189
190             # if we're supposed to truncate large attachments
191             if ($RT::TruncateLongAttachments) {
192
193                 # truncate the attachment to that length.
194                 $Body = substr( $Body, 0, $MaxSize );
195
196             }
197
198             # elsif we're supposed to drop large attachments on the floor,
199             elsif ($RT::DropLongAttachments) {
200
201                 # drop the attachment on the floor
202                 $RT::Logger->info( "$self: Dropped an attachment of size " . length($Body) . "\n" . "It started: " . substr( $Body, 0, 60 ) . "\n" );
203                 return (undef);
204             }
205         }
206
207         # if we need to mimencode the attachment
208         if ( $ContentEncoding eq 'base64' ) {
209
210             # base64 encode the attachment
211             Encode::_utf8_off($Body);
212             $Body = MIME::Base64::encode_base64($Body);
213
214         } elsif ($ContentEncoding eq 'quoted-printable') {
215             Encode::_utf8_off($Body);
216             $Body = MIME::QuotedPrint::encode($Body);
217         }
218
219
220         my $id = $self->SUPER::Create( TransactionId => $args{'TransactionId'},
221                                        ContentType   => $Attachment->mime_type,
222                                        ContentEncoding => $ContentEncoding,
223                                        Parent          => $args{'Parent'},
224                                        Headers       =>  $Attachment->head->as_string, 
225                                        Subject       =>  $Subject,
226                                        Content         => $Body,
227                                        Filename => $Filename, );
228         return ($id);
229     }
230 }
231
232 # }}}
233
234
235 =head2 Import
236
237 Create an attachment exactly as specified in the named parameters.
238
239 =cut
240
241
242 sub Import {
243     my $self = shift;
244     return($self->SUPER::Create(@_));
245 }
246
247 # {{{ sub Content
248
249 =head2 Content
250
251 Returns the attachment's content. if it's base64 encoded, decode it 
252 before returning it.
253
254 =cut
255
256 sub Content {
257   my $self = shift;
258   my $decode_utf8 = (($self->ContentType eq 'text/plain') ? 1 : 0);
259
260   if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
261       return $self->_Value(
262           'Content',
263           decode_utf8 => $decode_utf8,
264       );
265   } elsif ( $self->ContentEncoding eq 'base64' ) {
266       return ( $decode_utf8
267         ? Encode::decode_utf8(MIME::Base64::decode_base64($self->_Value('Content')))
268         : MIME::Base64::decode_base64($self->_Value('Content'))
269       );
270   } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
271       return ( $decode_utf8
272         ? Encode::decode_utf8(MIME::QuotedPrint::decode($self->_Value('Content')))
273         : MIME::QuotedPrint::decode($self->_Value('Content'))
274       );
275   } else {
276       return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
277   }
278 }
279
280
281 # }}}
282
283
284 # {{{ sub OriginalContent
285
286 =head2 OriginalContent
287
288 Returns the attachment's content as octets before RT's mangling.
289 Currently, this just means restoring text/plain content back to its
290 original encoding.
291
292 =cut
293
294 sub OriginalContent {
295   my $self = shift;
296
297   return $self->Content unless $self->ContentType eq 'text/plain';
298   my $enc = $self->OriginalEncoding;
299
300   my $content;
301   if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
302       $content = $self->_Value('Content', decode_utf8 => 0);
303   } elsif ( $self->ContentEncoding eq 'base64' ) {
304       $content = MIME::Base64::decode_base64($self->_Value('Content', decode_utf8 => 0));
305   } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
306       return MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
307   } else {
308       return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
309   }
310
311   # Encode::_utf8_on($content);
312   if (!$enc or $enc eq 'utf8' or $enc eq 'utf-8') {
313     # If we somehow fail to do the decode, at least push out the raw bits
314     eval {return( Encode::decode_utf8($content))} || return ($content);
315   }
316   Encode::from_to($content, 'utf8' => $enc);
317   return $content;
318 }
319
320 # }}}
321
322
323 # {{{ sub OriginalEncoding
324
325 =head2 OriginalEncoding
326
327 Returns the attachment's original encoding.
328
329 =cut
330
331 sub OriginalEncoding {
332   my $self = shift;
333   return $self->GetHeader('X-RT-Original-Encoding');
334 }
335
336 # }}}
337
338 # {{{ sub Children
339
340 =head2 Children
341
342   Returns an RT::Attachments object which is preloaded with all Attachments objects with this Attachment\'s Id as their 'Parent'
343
344 =cut
345
346 sub Children {
347     my $self = shift;
348     
349     my $kids = new RT::Attachments($self->CurrentUser);
350     $kids->ChildrenOf($self->Id);
351     return($kids);
352 }
353
354 # }}}
355
356 # {{{ UTILITIES
357
358 # {{{ sub Quote 
359
360
361
362 sub Quote {
363     my $self=shift;
364     my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
365               @_);
366
367     my ($quoted_content, $body, $headers);
368     my $max=0;
369
370     # TODO: Handle Multipart/Mixed (eventually fix the link in the
371     # ShowHistory web template?)
372     if ($self->ContentType =~ m{^(text/plain|message)}i) {
373         $body=$self->Content;
374
375         # Do we need any preformatting (wrapping, that is) of the message?
376
377         # Remove quoted signature.
378         $body =~ s/\n-- \n(.*)$//s;
379
380         # What's the longest line like?
381         foreach (split (/\n/,$body)) {
382             $max=length if ( length > $max);
383         }
384
385         if ($max>76) {
386             require Text::Wrapper;
387             my $wrapper=new Text::Wrapper
388                 (
389                  columns => 70, 
390                  body_start => ($max > 70*3 ? '   ' : ''),
391                  par_start => ''
392                  );
393             $body=$wrapper->wrap($body);
394         }
395
396         $body =~ s/^/> /gm;
397
398         $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
399                     . "]:\n\n"
400                 . $body . "\n\n";
401
402     } else {
403         $body = "[Non-text message not quoted]\n\n";
404     }
405     
406     $max=60 if $max<60;
407     $max=70 if $max>78;
408     $max+=2;
409
410     return (\$body, $max);
411 }
412 # }}}
413
414 # {{{ sub NiceHeaders - pulls out only the most relevant headers
415
416 =head2 NiceHeaders
417
418 Returns the To, From, Cc, Date and Subject headers.
419
420 It is a known issue that this breaks if any of these headers are not
421 properly unfolded.
422
423 =cut
424
425 sub NiceHeaders {
426     my $self=shift;
427     my $hdrs="";
428     for (split(/\n/,$self->Headers)) {
429             $hdrs.="$_\n" if /^(To|From|RT-Send-Cc|Cc|Date|Subject): /i
430     }
431     return $hdrs;
432 }
433 # }}}
434
435 # {{{ sub Headers
436
437 =head2 Headers
438
439 Returns this object's headers as a string.  This method specifically
440 removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
441 We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
442 out mail. (The mailing rules are seperated from the ticket update code by
443 an abstraction barrier that makes it impossible to pass this data directly
444
445 =cut
446
447 sub Headers {
448     my $self = shift;
449     my $hdrs="";
450     for (split(/\n/,$self->SUPER::Headers)) {
451             $hdrs.="$_\n" unless /^(RT-Send-Bcc): /i
452     }
453     return $hdrs;
454 }
455
456
457 # }}}
458
459 # {{{ sub GetHeader
460
461 =head2 GetHeader ( 'Tag')
462
463 Returns the value of the header Tag as a string. This bypasses the weeding out
464 done in Headers() above.
465
466 =cut
467
468 sub GetHeader {
469     my $self = shift;
470     my $tag = shift;
471     foreach my $line (split(/\n/,$self->SUPER::Headers)) {
472         if ($line =~ /^\Q$tag\E:\s+(.*)$/i) { #if we find the header, return its value
473             return ($1);
474         }
475     }
476     
477     # we found no header. return an empty string
478     return undef;
479 }
480 # }}}
481
482 # {{{ sub SetHeader
483
484 =head2 SetHeader ( 'Tag', 'Value' )
485
486 Replace or add a Header to the attachment's headers.
487
488 =cut
489
490 sub SetHeader {
491     my $self = shift;
492     my $tag = shift;
493     my $newheader = '';
494
495     foreach my $line (split(/\n/,$self->SUPER::Headers)) {
496         if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
497             $newheader .= "$tag: $_[0]\n";
498             undef $tag;
499         }
500         else {
501             $newheader .= "$line\n";
502         }
503     }
504
505     $newheader .= "$tag: $_[0]\n" if defined $tag;
506     $self->__Set( Field => 'Headers', Value => $newheader);
507 }
508 # }}}
509
510 # {{{ sub _Value 
511
512 =head2 _Value
513
514 Takes the name of a table column.
515 Returns its value as a string, if the user passes an ACL check
516
517 =cut
518
519 sub _Value  {
520
521     my $self = shift;
522     my $field = shift;
523     
524     
525     #if the field is public, return it.
526     if ($self->_Accessible($field, 'public')) {
527         #$RT::Logger->debug("Skipping ACL check for $field\n");
528         return($self->__Value($field, @_));
529         
530     }
531     
532     #If it's a comment, we need to be extra special careful
533     elsif ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
534              ($self->TransactionObj->Type eq 'Comment') )  or
535             ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
536                 return($self->__Value($field, @_));
537     }
538     #if they ain't got rights to see, don't let em
539     else {
540             return(undef);
541         }
542         
543     
544 }
545
546 # }}}
547
548 sub ContentLength {
549     my $self = shift;
550
551     unless ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
552              ($self->TransactionObj->Type eq 'Comment') )  or
553             ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
554         return undef;
555     }
556
557     if (my $len = $self->GetHeader('Content-Length')) {
558         return $len;
559     }
560
561     {
562         use bytes;
563         my $len = length($self->Content);
564         $self->SetHeader('Content-Length' => $len);
565         return $len;
566     }
567 }
568
569 # }}}
570
571 1;