1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC
6#                                          <sales@bestpractical.com>
7#
8# (Except where explicitly superseded by other copyright notices)
9#
10#
11# LICENSE:
12#
13# This work is made available to you under the terms of Version 2 of
14# the GNU General Public License. A copy of that license should have
15# been provided with this software, but in any event can be snarfed
16# from www.gnu.org.
17#
18# This work is distributed in the hope that it will be useful, but
19# WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21# General Public License for more details.
22#
23# You should have received a copy of the GNU General Public License
24# along with this program; if not, write to the Free Software
25# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26# 02110-1301 or visit their web page on the internet at
27# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28#
29#
30# CONTRIBUTION SUBMISSION POLICY:
31#
32# (The following paragraph is not intended to limit the rights granted
33# to you to modify and distribute this software under the terms of
34# the GNU General Public License and is only of importance to you if
35# you choose to contribute your changes and enhancements to the
36# community by submitting them to Best Practical Solutions, LLC.)
37#
38# By intentionally submitting any modifications, corrections or
39# derivatives to this work, or any other work intended for use with
40# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41# you are the copyright holder for those contributions and you grant
42# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43# royalty-free, perpetual, license to use, copy, create derivative
44# works based on those contributions, and sublicense and distribute
45# those contributions and any derivatives thereof.
46#
47# END BPS TAGGED BLOCK }}}
48
49=head1 NAME
50
51  RT::Attachments - a collection of RT::Attachment objects
52
53=head1 SYNOPSIS
54
55  use RT::Attachments;
56
57=head1 DESCRIPTION
58
59This module should never be called directly by client code. it's an internal module which
60should only be accessed through exported APIs in Ticket, Queue and other similar objects.
61
62
63=head1 METHODS
64
65
66
67=cut
68
69
70package RT::Attachments;
71use strict;
72use warnings;
73
74use base 'RT::SearchBuilder';
75
76use RT::Attachment;
77
78sub Table { 'Attachments'}
79
80
81use RT::Attachment;
82
83sub _Init   {
84    my $self = shift;
85    $self->{'table'} = "Attachments";
86    $self->{'primary_key'} = "id";
87    $self->OrderBy(
88        FIELD => 'id',
89        ORDER => 'ASC',
90    );
91    return $self->SUPER::_Init( @_ );
92}
93
94sub CleanSlate {
95    my $self = shift;
96    delete $self->{_sql_transaction_alias};
97    return $self->SUPER::CleanSlate( @_ );
98}
99
100
101=head2 TransactionAlias
102
103Returns alias for transactions table with applied join condition.
104Always return the same alias, so if you want to build some complex
105or recursive joining then you have to create new alias youself.
106
107=cut
108
109sub TransactionAlias {
110    my $self = shift;
111    return $self->{'_sql_transaction_alias'}
112        if $self->{'_sql_transaction_alias'};
113
114    return $self->{'_sql_transaction_alias'} = $self->Join(
115        ALIAS1 => 'main',
116        FIELD1 => 'TransactionId',
117        TABLE2 => 'Transactions',
118        FIELD2 => 'id',
119    );
120}
121
122=head2 ContentType (VALUE => 'text/plain', ENTRYAGGREGATOR => 'OR', OPERATOR => '=' )
123
124Limit result set to attachments of ContentType 'TYPE'...
125
126=cut
127
128
129sub ContentType  {
130    my $self = shift;
131    my %args = (
132        VALUE           => 'text/plain',
133        OPERATOR        => '=',
134        ENTRYAGGREGATOR => 'OR',
135        @_
136    );
137
138    return $self->Limit ( %args, FIELD => 'ContentType' );
139}
140
141=head2 ChildrenOf ID
142
143Limit result set to children of Attachment ID
144
145=cut
146
147
148sub ChildrenOf  {
149    my $self = shift;
150    my $attachment = shift;
151    return $self->Limit(
152        FIELD => 'Parent',
153        VALUE => $attachment
154    );
155}
156
157=head2 LimitNotEmpty
158
159Limit result set to attachments with not empty content.
160
161=cut
162
163sub LimitNotEmpty {
164    my $self = shift;
165    $self->Limit(
166        ENTRYAGGREGATOR => 'AND',
167        FIELD           => 'Content',
168        OPERATOR        => 'IS NOT',
169        VALUE           => 'NULL',
170        QUOTEVALUE      => 0,
171    );
172
173    # http://rt3.fsck.com/Ticket/Display.html?id=12483
174    if ( RT->Config->Get('DatabaseType') ne 'Oracle' ) {
175        $self->Limit(
176            ENTRYAGGREGATOR => 'AND',
177            FIELD           => 'Content',
178            OPERATOR        => '!=',
179            VALUE           => '',
180        );
181    }
182    return;
183}
184
185=head2 LimitHasFilename
186
187Limit result set to attachments with not empty filename.
188
189=cut
190
191sub LimitHasFilename {
192    my $self = shift;
193
194    $self->Limit(
195        ENTRYAGGREGATOR => 'AND',
196        FIELD           => 'Filename',
197        OPERATOR        => 'IS NOT',
198        VALUE           => 'NULL',
199        QUOTEVALUE      => 0,
200    );
201
202    if ( RT->Config->Get('DatabaseType') ne 'Oracle' ) {
203        $self->Limit(
204            ENTRYAGGREGATOR => 'AND',
205            FIELD           => 'Filename',
206            OPERATOR        => '!=',
207            VALUE           => '',
208        );
209    }
210
211    return;
212}
213
214=head2 LimitByTicket $ticket_id
215
216Limit result set to attachments of a ticket.
217
218=cut
219
220sub LimitByTicket {
221    my $self = shift;
222    my $tid = shift;
223
224    my $transactions = $self->TransactionAlias;
225    $self->Limit(
226        ENTRYAGGREGATOR => 'AND',
227        ALIAS           => $transactions,
228        FIELD           => 'ObjectType',
229        VALUE           => 'RT::Ticket',
230    );
231
232    my $tickets = $self->Join(
233        ALIAS1 => $transactions,
234        FIELD1 => 'ObjectId',
235        TABLE2 => 'Tickets',
236        FIELD2 => 'id',
237    );
238    $self->Limit(
239        ENTRYAGGREGATOR => 'AND',
240        ALIAS           => $tickets,
241        FIELD           => 'EffectiveId',
242        VALUE           => $tid,
243    );
244    return;
245}
246
247sub AddRecord {
248    my $self = shift;
249    my ($record) = @_;
250
251    return unless $record->TransactionObj->CurrentUserCanSee;
252    return $self->SUPER::AddRecord( $record );
253}
254
255=head2 ReplaceAttachments ( Search => 'SEARCH', Replacement => 'Replacement', Header => 1, Content => 1 )
256
257Provide a search string to search the attachments table for, by default the Headers and Content
258columns will both be searched for matches.
259
260=cut
261
262sub ReplaceAttachments {
263    my $self = shift;
264    my %args = (
265        Search      => undef,
266        Replacement => '',
267        Headers     => 1,
268        Content     => 1,
269        FilterBySearchString => 1,
270        @_,
271    );
272
273    return ( 0, $self->loc('Provide a search string to search on') ) unless $args{Search};
274
275
276    my %munged;
277    my $create_munge_txn = sub {
278        my $ticket = shift;
279        if ( !$munged{ $ticket->id } ) {
280            my ( $ret, $msg ) = $ticket->_NewTransaction( Type => "Munge" );
281            if ($ret) {
282                $munged{ $ticket->id } = 1;
283            }
284            else {
285                RT::Logger->error($msg);
286            }
287        }
288    };
289
290    my $attachments = $self->Clone;
291    if ( $args{FilterBySearchString} ) {
292        $attachments->Limit(
293            FIELD     => 'ContentEncoding',
294            VALUE     => 'none',
295            SUBCLAUSE => 'Encoding',
296        );
297        $attachments->Limit(
298            FIELD     => 'ContentEncoding',
299            OPERATOR  => 'IS',
300            VALUE     => 'NULL',
301            SUBCLAUSE => 'Encoding',
302        );
303
304        # For QP encoding, if encoded string is equal to the decoded
305        # version, then SQL search will also work.
306        #
307        # Adding "\n" is to avoid trailing "=" in QP encoding
308        if ( MIME::QuotedPrint::encode( Encode::encode( 'UTF-8', "$args{Search}\n" ) ) eq
309            Encode::encode( 'UTF-8', "$args{Search}\n" ) )
310        {
311            $attachments->Limit(
312                FIELD     => 'ContentEncoding',
313                VALUE     => 'quoted-printable',
314                SUBCLAUSE => 'Encoding',
315            );
316        }
317    }
318
319    if ( $args{Headers} ) {
320        my $atts = $attachments->Clone;
321        if ( $args{FilterBySearchString} ) {
322            $atts->Limit(
323                FIELD    => 'Headers',
324                OPERATOR => 'LIKE',
325                VALUE    => $args{Search},
326            );
327        }
328        $atts->Limit(
329            FIELD     => 'ContentType',
330            OPERATOR  => 'IN',
331            VALUE     => [ RT::Util::EmailContentTypes(), 'text/plain', 'text/html' ],
332            SUBCLAUSE => 'Types',
333        );
334        $atts->Limit(
335            FIELD           => 'ContentType',
336            OPERATOR        => 'STARTSWITH',
337            VALUE           => 'multipart/',
338            SUBCLAUSE       => 'Types',
339            ENTRYAGGREGATOR => 'OR',
340        );
341
342        while ( my $att = $atts->Next ) {
343            my ( $ret, $msg ) = $att->ReplaceHeaders(
344                Search      => $args{Search},
345                Replacement => $args{Replacement},
346            );
347
348            if ( $ret ) {
349                $create_munge_txn->( $att->TransactionObj->TicketObj );
350            }
351            else {
352                RT::Logger->debug($msg);
353            }
354        }
355    }
356
357    if ( $args{Content} ) {
358        my $atts = $attachments->Clone;
359        if ( $args{FilterBySearchString} ) {
360            $atts->Limit(
361                FIELD     => 'Content',
362                OPERATOR  => 'LIKE',
363                VALUE     => $args{Search},
364                SUBCLAUSE => 'Content',
365            );
366        }
367        $atts->Limit(
368            FIELD    => 'ContentType',
369            OPERATOR => 'IN',
370            VALUE    => [ 'text/plain', 'text/html' ],
371        );
372
373        while ( my $att = $atts->Next ) {
374            my ( $ret, $msg ) = $att->ReplaceContent(
375                Search      => $args{Search},
376                Replacement => $args{Replacement},
377            );
378
379            if ( $ret ) {
380                $create_munge_txn->( $att->TransactionObj->TicketObj );
381            }
382            else {
383                RT::Logger->debug($msg);
384            }
385        }
386    }
387
388    my $count = scalar keys %munged;
389    return wantarray ? ( 1, $self->loc( "Updated [quant,_1,ticket's,tickets'] attachment content", $count ) ) : $count;
390}
391
392RT::Base->_ImportOverlays();
393
3941;
395