#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
use RT::I18N;
use RT::User;
use RT::Attributes;
-use Encode qw();
our $_TABLE_ATTR = { };
use base RT->Config->Get('RecordBaseClass');
return undef if (!defined $value);
+ # Pg returns character columns as character strings; mysql and
+ # sqlite return them as bytes. While mysql can be made to return
+ # characters, using the mysql_enable_utf8 flag, the "Content" column
+ # is bytes on mysql and characters on Postgres, making true
+ # consistency impossible.
if ( $args{'decode_utf8'} ) {
- if ( !utf8::is_utf8($value) ) {
+ if ( !utf8::is_utf8($value) ) { # mysql/sqlite
utf8::decode($value);
}
- }
- else {
+ } else {
if ( utf8::is_utf8($value) ) {
utf8::encode($value);
}
sub _CacheConfig {
{
- 'cache_p' => 1,
'cache_for_sec' => 30,
}
}
}
-=head2 _EncodeLOB BODY MIME_TYPE
+=head2 _EncodeLOB BODY MIME_TYPE FILENAME
+
+Takes a potentially large attachment. Returns (ContentEncoding,
+EncodedBody, MimeType, Filename) based on system configuration and
+selected database. Returns a custom (short) text/plain message if
+DropLongAttachments causes an attachment to not be stored.
+
+Encodes your data as base64 or Quoted-Printable as needed based on your
+Databases's restrictions and the UTF-8ness of the data being passed in. Since
+we are storing in columns marked UTF8, we must ensure that binary data is
+encoded on databases which are strict.
-Takes a potentially large attachment. Returns (ContentEncoding, EncodedBody) based on system configuration and selected database
+This function expects to receive an octet string in order to properly
+evaluate and encode it. It will return an octet string.
=cut
sub _EncodeLOB {
- my $self = shift;
- my $Body = shift;
- my $MIMEType = shift || '';
- my $Filename = shift;
+ my $self = shift;
+ my $Body = shift;
+ my $MIMEType = shift || '';
+ my $Filename = shift;
- my $ContentEncoding = 'none';
+ my $ContentEncoding = 'none';
- #get the max attachment length from RT
- my $MaxSize = RT->Config->Get('MaxAttachmentSize');
+ RT::Util::assert_bytes( $Body );
- #if the current attachment contains nulls and the
- #database doesn't support embedded nulls
+ #get the max attachment length from RT
+ my $MaxSize = RT->Config->Get('MaxAttachmentSize');
- if ( ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
+ #if the current attachment contains nulls and the
+ #database doesn't support embedded nulls
- # set a flag telling us to mimencode the attachment
- $ContentEncoding = 'base64';
+ if ( ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
- #cut the max attchment size by 25% (for mime-encoding overhead.
- $RT::Logger->debug("Max size is $MaxSize");
- $MaxSize = $MaxSize * 3 / 4;
- # Some databases (postgres) can't handle non-utf8 data
- } elsif ( !$RT::Handle->BinarySafeBLOBs
- && $MIMEType !~ /text\/plain/gi
- && !Encode::is_utf8( $Body, 1 ) ) {
- $ContentEncoding = 'quoted-printable';
- }
+ # set a flag telling us to mimencode the attachment
+ $ContentEncoding = 'base64';
- #if the attachment is larger than the maximum size
- if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
+ #cut the max attchment size by 25% (for mime-encoding overhead.
+ $RT::Logger->debug("Max size is $MaxSize");
+ $MaxSize = $MaxSize * 3 / 4;
+ # Some databases (postgres) can't handle non-utf8 data
+ } elsif ( !$RT::Handle->BinarySafeBLOBs
+ && $Body =~ /\P{ASCII}/
+ && !Encode::is_utf8( $Body, 1 ) ) {
+ $ContentEncoding = 'quoted-printable';
+ }
- # if we're supposed to truncate large attachments
- if (RT->Config->Get('TruncateLongAttachments')) {
+ #if the attachment is larger than the maximum size
+ if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
- # truncate the attachment to that length.
- $Body = substr( $Body, 0, $MaxSize );
+ # if we're supposed to truncate large attachments
+ if (RT->Config->Get('TruncateLongAttachments')) {
- }
+ # truncate the attachment to that length.
+ $Body = substr( $Body, 0, $MaxSize );
- # elsif we're supposed to drop large attachments on the floor,
- elsif (RT->Config->Get('DropLongAttachments')) {
-
- # drop the attachment on the floor
- $RT::Logger->info( "$self: Dropped an attachment of size "
- . length($Body));
- $RT::Logger->info( "It started: " . substr( $Body, 0, 60 ) );
- $Filename .= ".txt" if $Filename;
- return ("none", "Large attachment dropped", "plain/text", $Filename );
- }
}
- # if we need to mimencode the attachment
- if ( $ContentEncoding eq 'base64' ) {
+ # elsif we're supposed to drop large attachments on the floor,
+ elsif (RT->Config->Get('DropLongAttachments')) {
- # base64 encode the attachment
- Encode::_utf8_off($Body);
- $Body = MIME::Base64::encode_base64($Body);
-
- } elsif ($ContentEncoding eq 'quoted-printable') {
- Encode::_utf8_off($Body);
- $Body = MIME::QuotedPrint::encode($Body);
+ # drop the attachment on the floor
+ $RT::Logger->info( "$self: Dropped an attachment of size "
+ . length($Body));
+ $RT::Logger->info( "It started: " . substr( $Body, 0, 60 ) );
+ $Filename .= ".txt" if $Filename;
+ return ("none", "Large attachment dropped", "text/plain", $Filename );
}
+ }
+ # if we need to mimencode the attachment
+ if ( $ContentEncoding eq 'base64' ) {
+ # base64 encode the attachment
+ $Body = MIME::Base64::encode_base64($Body);
- return ($ContentEncoding, $Body, $MIMEType, $Filename );
+ } elsif ($ContentEncoding eq 'quoted-printable') {
+ $Body = MIME::QuotedPrint::encode($Body);
+ }
+ return ($ContentEncoding, $Body, $MIMEType, $Filename );
}
+=head2 _DecodeLOB C<ContentType>, C<ContentEncoding>, C<Content>
+
+Unpacks data stored in the database, which may be base64 or QP encoded
+because of our need to store binary and badly encoded data in columns
+marked as UTF-8. Databases such as PostgreSQL and Oracle care that you
+are feeding them invalid UTF-8 and will refuse the content. This
+function handles unpacking the encoded data.
+
+It returns textual data as a UTF-8 string which has been processed by Encode's
+PERLQQ filter which will replace the invalid bytes with \x{HH} so you can see
+the invalid byte but won't run into problems treating the data as UTF-8 later.
+
+This is similar to how we filter all data coming in via the web UI in
+RT::Interface::Web::DecodeARGS. This filter should only end up being
+applied to old data from less UTF-8-safe versions of RT.
+
+If the passed C<ContentType> includes a character set, that will be used
+to decode textual data; the default character set is UTF-8. This is
+necessary because while we attempt to store textual data as UTF-8, the
+definition of "textual" has migrated over time, and thus we may now need
+to attempt to decode data that was previously not trancoded on insertion.
+
+Important Note - This function expects an octet string and returns a
+character string for non-binary data.
+
+=cut
+
sub _DecodeLOB {
my $self = shift;
my $ContentType = shift || '';
my $ContentEncoding = shift || 'none';
my $Content = shift;
+ RT::Util::assert_bytes( $Content );
+
if ( $ContentEncoding eq 'base64' ) {
$Content = MIME::Base64::decode_base64($Content);
}
return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) );
}
if ( RT::I18N::IsTextualContentType($ContentType) ) {
- $Content = Encode::decode_utf8($Content) unless Encode::is_utf8($Content);
+ my $entity = MIME::Entity->new();
+ $entity->head->add("Content-Type", $ContentType);
+ $entity->bodyhandle( MIME::Body::Scalar->new( $Content ) );
+ my $charset = RT::I18N::_FindOrGuessCharset($entity);
+ $charset = 'utf-8' if not $charset or not Encode::find_encoding($charset);
+
+ $Content = Encode::decode($charset,$Content,Encode::FB_PERLQQ);
}
- return ($Content);
+ return ($Content);
}
# A helper table for links mapping to make it easier
if ( $args{'Base'} and $args{'Target'} ) {
$RT::Logger->debug( "$self tried to create a link. both base and target were specified" );
- return ( 0, $self->loc("Can't specifiy both base and target") );
+ return ( 0, $self->loc("Can't specify both base and target") );
}
elsif ( $args{'Base'} ) {
$args{'Target'} = $self->URI();
if ( $args{'Base'} and $args{'Target'} ) {
$RT::Logger->debug("$self ->_DeleteLink. got both Base and Target");
- return ( 0, $self->loc("Can't specifiy both base and target") );
+ return ( 0, $self->loc("Can't specify both base and target") );
}
elsif ( $args{'Base'} ) {
$args{'Target'} = $self->URI();
sub CustomFieldLookupType {
my $self = shift;
- return ref($self);
+ return ref($self) || $self;
}
$i++;
if ( $i < $cf_values ) {
my ( $val, $msg ) = $cf->DeleteValueForObject(
- Object => $self,
- Content => $value->Content
+ Object => $self,
+ Id => $value->id,
);
unless ($val) {
return ( 0, $msg );
$values->RedoSearch if $i; # redo search if have deleted at least one value
}
- my ( $old_value, $old_content );
- if ( $old_value = $values->First ) {
- $old_content = $old_value->Content;
- $old_content = undef if defined $old_content && !length $old_content;
-
- my $is_the_same = 1;
- if ( defined $args{'Value'} ) {
- $is_the_same = 0 unless defined $old_content
- && lc $old_content eq lc $args{'Value'};
- } else {
- $is_the_same = 0 if defined $old_content;
- }
- if ( $is_the_same ) {
- my $old_content = $old_value->LargeContent;
- if ( defined $args{'LargeContent'} ) {
- $is_the_same = 0 unless defined $old_content
- && $old_content eq $args{'LargeContent'};
- } else {
- $is_the_same = 0 if defined $old_content;
- }
- }
-
- return $old_value->id if $is_the_same;
+ if ( my $entry = $values->HasEntry($args{'Value'}, $args{'LargeContent'}) ) {
+ return $entry->id;
}
+ my $old_value = $values->First;
+ my $old_content;
+ $old_content = $old_value->Content if $old_value;
+
my ( $new_value_id, $value_msg ) = $cf->AddValueForObject(
Object => $self,
Content => $args{'Value'},
# otherwise, just add a new value and record "new value added"
else {
+ if ( !$cf->Repeated ) {
+ my $values = $cf->ValuesForObject($self);
+ if ( my $entry = $values->HasEntry($args{'Value'}, $args{'LargeContent'}) ) {
+ return $entry->id;
+ }
+ }
+
my ($new_value_id, $msg) = $cf->AddValueForObject(
Object => $self,
Content => $args{'Value'},