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