diff --git a/Kernel/GenericInterface/Operation/Ticket/Common.pm b/Kernel/GenericInterface/Operation/Ticket/Common.pm index 022d988a24b..cc44e1bf0b1 100644 --- a/Kernel/GenericInterface/Operation/Ticket/Common.pm +++ b/Kernel/GenericInterface/Operation/Ticket/Common.pm @@ -1178,7 +1178,7 @@ sub SetDynamicFieldValue { if ( !$ObjectID ) { return { Success => 0, - ErrorMessage => "SetDynamicFieldValue() Could not set $ObjectID!", + ErrorMessage => "SetDynamicFieldValue() Could not set ObjectID!", }; } diff --git a/Kernel/GenericInterface/Operation/Ticket/TicketCreate.pm b/Kernel/GenericInterface/Operation/Ticket/TicketCreate.pm index 1af72ceef6e..a875b756899 100644 --- a/Kernel/GenericInterface/Operation/Ticket/TicketCreate.pm +++ b/Kernel/GenericInterface/Operation/Ticket/TicketCreate.pm @@ -512,39 +512,21 @@ sub Run { } # isolate Article parameter - my @Article; + my @Articles; if ( IsHashRefWithData( $Param{Data}->{Article} ) ) { - push @Article, $Param{Data}->{Article}; + push @Articles, $Param{Data}->{Article}; } - if ( IsArrayRefWithData( $Param{Data}->{Article} ) ) { - @Article = @{ $Param{Data}->{Article} }; + else { + @Articles = @{ $Param{Data}->{Article} }; } - for my $Article (@Article) { + for my $Article (@Articles) { $Article->{UserType} = $UserType; # remove leading and trailing spaces - for my $Attribute ( sort keys %{$Article} ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $Article->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $Article->{$Attribute} =~ s{\s+\z}{}; - } - } + s{ (?: \A\s+ | \s+\z ) }{}gx for values %$Article; if ( IsHashRefWithData( $Article->{OrigHeader} ) ) { - for my $Attribute ( sort keys %{ $Article->{OrigHeader} } ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $Article->{OrigHeader}->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $Article->{OrigHeader}->{$Attribute} =~ s{\s+\z}{}; - } - } + s{ (?: \A\s+ | \s+\z ) }{}gx for values %{ $Article->{OrigHeader} }; } # Check attributes that can be set by sysconfig. @@ -592,11 +574,11 @@ sub Run { $DynamicField = $Param{Data}->{DynamicField}; # homogenate input to array - if ( ref $DynamicField eq 'HASH' ) { - push @DynamicFieldList, $DynamicField; + if ( ref $DynamicField eq 'ARRAY' ) { + @DynamicFieldList = @{$DynamicField}; } else { - @DynamicFieldList = @{$DynamicField}; + push @DynamicFieldList, $DynamicField; } # check DynamicField internal structure @@ -610,16 +592,7 @@ sub Run { } # remove leading and trailing spaces - for my $Attribute ( sort keys %{$DynamicFieldItem} ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $DynamicFieldItem->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $DynamicFieldItem->{$Attribute} =~ s{\s+\z}{}; - } - } + s{ (?: \A\s+ | \s+\z ) }{}gx for values %{$DynamicFieldItem}; # check DynamicField attribute values my $DynamicFieldCheck = $Self->_CheckDynamicField( DynamicField => $DynamicFieldItem ); @@ -657,16 +630,7 @@ sub Run { } # remove leading and trailing spaces - for my $Attribute ( sort keys %{$AttachmentItem} ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $AttachmentItem->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $AttachmentItem->{$Attribute} =~ s{\s+\z}{}; - } - } + s{ (?: \A\s+ | \s+\z ) }{}gx for values %{$AttachmentItem}; # check Attachment attribute values my $AttachmentCheck = $Self->_CheckAttachment( Attachment => $AttachmentItem ); @@ -679,7 +643,7 @@ sub Run { return $Self->_TicketCreate( Ticket => $Ticket, - Article => \@Article, + Article => \@Articles, DynamicFieldList => \@DynamicFieldList, AttachmentList => \@AttachmentList, UserID => $UserID, diff --git a/Kernel/GenericInterface/Operation/Ticket/TicketUpdate.pm b/Kernel/GenericInterface/Operation/Ticket/TicketUpdate.pm index cde5d47305d..86b10341b5f 100644 --- a/Kernel/GenericInterface/Operation/Ticket/TicketUpdate.pm +++ b/Kernel/GenericInterface/Operation/Ticket/TicketUpdate.pm @@ -1,4 +1,4 @@ -# -- +#2422 -- # Copyright (C) 2001-2021 OTRS AG, https://otrs.com/ # Copyright (C) 2021 Znuny GmbH, https://znuny.org/ # Copyright (C) 2021 maxence business consulting GmbH, http://www.maxence.de @@ -453,17 +453,11 @@ sub Run { } # check optional hashes - for my $Optional (qw(Ticket Article)) { - if ( - defined $Param{Data}->{$Optional} - && !IsHashRefWithData( $Param{Data}->{$Optional} ) - ) - { - return $Self->ReturnError( - ErrorCode => 'TicketUpdate.InvalidParameter', - ErrorMessage => "TicketUpdate: $Optional parameter is not valid!", - ); - } + if ( defined $Param{Data}->{Ticket} && !IsHashRefWithData( $Param{Data}->{Ticket} ) ) { + return $Self->ReturnError( + ErrorCode => 'TicketUpdate.InvalidParameter', + ErrorMessage => "TicketUpdate: Ticket parameter is not valid!", + ); } # check optional array/hashes @@ -490,27 +484,9 @@ sub Run { $Ticket->{UserID} = $UserID; # remove leading and trailing spaces - for my $Attribute ( sort keys %{$Ticket} ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $Ticket->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $Ticket->{$Attribute} =~ s{\s+\z}{}; - } - } + s{ (?: \A\s+ | \s+\z ) }{}gx for values %$Ticket; if ( IsHashRefWithData( $Ticket->{PendingTime} ) ) { - for my $Attribute ( sort keys %{ $Ticket->{PendingTime} } ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $Ticket->{PendingTime}->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $Ticket->{PendingTime}->{$Attribute} =~ s{\s+\z}{}; - } - } + s{ (?: \A\s+ | \s+\z ) }{}gx for values %{ $Ticket->{PendingTime} }; } # check Ticket attribute values @@ -524,34 +500,29 @@ sub Run { } } - my $Article; - if ( defined $Param{Data}->{Article} ) { + my @Articles; + if ( exists $Param{Data}->{Article} && defined $Param{Data}->{Article} ) { + if ( IsHashRefWithData( $Param{Data}->{Article} ) ) { + push @Articles, $Param{Data}->{Article}; + } + elsif ( IsArrayRefWithData( $Param{Data}->{Article} ) ) { + @Articles = @{ $Param{Data}->{Article} }; + } + else { + return $Self->ReturnError( + ErrorCode => 'TicketUpdate.InvalidParameter', + ErrorMessage => "TicketUpdate: Article parameter is not valid!", + ); + } + } - $Article = $Param{Data}->{Article}; + for my $Article (@Articles) { $Article->{UserType} = $UserType; # remove leading and trailing spaces - for my $Attribute ( sort keys %{$Article} ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $Article->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $Article->{$Attribute} =~ s{\s+\z}{}; - } - } + s{ (?: \A\s+ | \s+\z ) }{}gx for values %$Article; if ( IsHashRefWithData( $Article->{OrigHeader} ) ) { - for my $Attribute ( sort keys %{ $Article->{OrigHeader} } ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $Article->{OrigHeader}->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $Article->{OrigHeader}->{$Attribute} =~ s{\s+\z}{}; - } - } + s{ (?: \A\s+ | \s+\z ) }{}gx for values %{ $Article->{OrigHeader} }; } # Check attributes that can be set by sysconfig. @@ -588,54 +559,30 @@ sub Run { } return $Self->ReturnError( %{$ArticleCheck} ); } - } - - my $DynamicField; - my @DynamicFieldList; - if ( defined $Param{Data}->{DynamicField} ) { - - # isolate DynamicField parameter - $DynamicField = $Param{Data}->{DynamicField}; - - # homogenate input to array - if ( ref $DynamicField eq 'HASH' ) { - push @DynamicFieldList, $DynamicField; - } - else { - @DynamicFieldList = @{$DynamicField}; - } - # check DynamicField internal structure - for my $DynamicFieldItem (@DynamicFieldList) { - if ( !IsHashRefWithData($DynamicFieldItem) ) { + if ( $Article->{DynamicField} ) { + my $ArticleDynamicFieldList = _MakeArrayRef( $Article->{DynamicField} ); + my $DynFieldCheck = $Self->_ValidateDynamicFields( $ArticleDynamicFieldList, 1 ); + if ($DynFieldCheck) { return { - ErrorCode => 'TicketUpdate.InvalidParameter', - ErrorMessage => - "TicketUpdate: Ticket->DynamicField parameter is invalid!", + Success => 0, + %$DynFieldCheck, }; } + } + } - # remove leading and trailing spaces - for my $Attribute ( sort keys %{$DynamicFieldItem} ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $DynamicFieldItem->{$Attribute} =~ s{\A\s+}{}; - - #remove trailing spaces - $DynamicFieldItem->{$Attribute} =~ s{\s+\z}{}; - } - } - - # check DynamicField attribute values - my $DynamicFieldCheck = $Self->_CheckDynamicField( - DynamicField => $DynamicFieldItem, - Article => $Article, - ); - - if ( !$DynamicFieldCheck->{Success} ) { - return $Self->ReturnError( %{$DynamicFieldCheck} ); - } + # For backwards compatibility we have to interpret $Param{Data}->{DynamicField} as ticket dynamic fields if + # no article was sent, and article dynamic fields otherwise + my $DynamicFieldList; + if ( $Param{Data}->{DynamicField} ) { + $DynamicFieldList = _MakeArrayRef( $Param{Data}->{DynamicField} ); + my $DynFieldCheck = $Self->_ValidateDynamicFields( $DynamicFieldList, @Articles > 0 ); + if ($DynFieldCheck) { + return { + Success => 0, + %$DynFieldCheck, + }; } } @@ -653,49 +600,40 @@ sub Run { else { @AttachmentList = @{$Attachment}; } + } - # check Attachment internal structure - for my $AttachmentItem (@AttachmentList) { - if ( !IsHashRefWithData($AttachmentItem) ) { - return { - ErrorCode => 'TicketUpdate.InvalidParameter', - ErrorMessage => - "TicketUpdate: Ticket->Attachment parameter is invalid!", - }; - } - - # remove leading and trailing spaces - for my $Attribute ( sort keys %{$AttachmentItem} ) { - if ( ref $Attribute ne 'HASH' && ref $Attribute ne 'ARRAY' ) { - - #remove leading spaces - $AttachmentItem->{$Attribute} =~ s{\A\s+}{}; + # check Attachment internal structure + for my $AttachmentItem (@AttachmentList) { + if ( !IsHashRefWithData($AttachmentItem) ) { + return { + ErrorCode => 'TicketUpdate.InvalidParameter', + ErrorMessage => + "TicketUpdate: Ticket->Attachment parameter is invalid!", + }; + } - #remove trailing spaces - $AttachmentItem->{$Attribute} =~ s{\s+\z}{}; - } - } + # remove leading and trailing spaces + s{ (?: \A\s+ | \s+\z ) }{}gx for values %{$AttachmentItem}; - # check Attachment attribute values - my $AttachmentCheck = $Self->_CheckAttachment( - Attachment => $AttachmentItem, - Article => $Article, - ); + # check Attachment attribute values + my $AttachmentCheck = $Self->_CheckAttachment( + Attachment => $AttachmentItem, + Article => $Articles[0], # Just use the first article, _CheckAttachment doesn't really look at the contents + ); - if ( !$AttachmentCheck->{Success} ) { - return $Self->ReturnError( %{$AttachmentCheck} ); - } + if ( !$AttachmentCheck->{Success} ) { + return $Self->ReturnError( %{$AttachmentCheck} ); } } return $Self->_TicketUpdate( - TicketID => $TicketID, - Ticket => $Ticket, - Article => $Article, - DynamicFieldList => \@DynamicFieldList, - AttachmentList => \@AttachmentList, - UserID => $UserID, - UserType => $UserType, + TicketID => $TicketID, + Ticket => $Ticket, + Articles => \@Articles, + TicketDynamicFields => $DynamicFieldList, + AttachmentList => \@AttachmentList, + UserID => $UserID, + UserType => $UserType, ); } @@ -1168,37 +1106,73 @@ sub _CheckArticle { }; } -=head2 _CheckDynamicField() +=head2 _MakeArrayRef() -checks if the given dynamic field parameter is valid. +Returns its argument unmodified if it is a list ref, otherwise returns a +reference to a new single-element list containing the argument. - my $DynamicFieldCheck = $OperationObject->_CheckDynamicField( - DynamicField => $DynamicField, # all dynamic field parameters - ); +Simple function, not a method! - returns: +=cut - $DynamicFieldCheck = { - Success => 1, # if everything is OK - } +sub _MakeArrayRef { + my ($Item) = @_; + return $Item if ref $Item eq 'ARRAY'; + return [$Item]; +} - $DynamicFieldCheck = { - ErrorCode => 'Function.Error', # if error - ErrorMessage => 'Error description', - } +=head2 _ValidateDynamicFields() + +Validate a list of dynamic field specs. Also removes leading and trailing +spaces from DF values. Set C<$IsArticle> to 1 to validate an Article dynamic +field. + + $Check = $OperationObject->_ValidateDynamicFields($DynamicField); + $Check = $OperationObject->_ValidateDynamicFields($DynamicField, 1); + +Returns undef on success, a hash with an error message otherwise. =cut -sub _CheckDynamicField { - my ( $Self, %Param ) = @_; +sub _ValidateDynamicFields { + my ( $Self, $DynamicFieldList, $IsArticle ) = @_; + + return if !defined $DynamicFieldList; + + # check DynamicField internal structure + for my $DynamicFieldItem (@$DynamicFieldList) { + if ( !IsHashRefWithData($DynamicFieldItem) ) { + return { + ErrorCode => 'TicketUpdate.InvalidParameter', + ErrorMessage => + "TicketUpdate: Ticket->DynamicField parameter is invalid!", + }; + } - my $DynamicField = $Param{DynamicField}; - my $ArticleData = $Param{Article}; + # remove leading and trailing spaces + s{ (?: \A\s+ | \s+\z ) }{}gx for values %$DynamicFieldItem; + + # check DynamicField attribute values + my $DynamicFieldCheck = $Self->_CheckDynamicField( $DynamicFieldItem, $IsArticle ); - my $Article; - if ( IsHashRefWithData($ArticleData) ) { - $Article = 1; + return $Self->ReturnError(%$DynamicFieldCheck) if $DynamicFieldCheck; } + return; +} + +=head2 _CheckDynamicField() + +Checks if the given dynamic field parameter is valid. Set C<$IsArticle> to 1 to +validate an Article dynamic field. + + my $DynamicFieldCheck = $OperationObject->_CheckDynamicField($DynamicField); + +Returns undef on success, a hash with an error message otherwise. + +=cut + +sub _CheckDynamicField { + my ( $Self, $DynamicField, $IsArticle ) = @_; # check DynamicField item internally for my $Needed (qw(Name Value)) { @@ -1226,7 +1200,7 @@ sub _CheckDynamicField { if ( !$Self->ValidateDynamicFieldObjectType( %{$DynamicField}, - Article => $Article, + Article => $IsArticle, ) ) { @@ -1245,10 +1219,7 @@ sub _CheckDynamicField { }; } - # if everything is OK then return Success - return { - Success => 1, - }; + return; } =head2 _CheckAttachment() @@ -1296,7 +1267,7 @@ sub _CheckAttachment { } } - # check Article->ContentType + # check Attachment->ContentType if ( $Attachment->{ContentType} ) { # Internal issue #595: Remove line breaks from content type. @@ -1531,8 +1502,8 @@ updates a ticket and creates an article and sets dynamic fields and attachments my $Response = $OperationObject->_TicketUpdate( TicketID => 123, Ticket => $Ticket, # all ticket parameters - Article => $Article, # all attachment parameters - DynamicField => $DynamicField, # all dynamic field parameters + Articles => @Articles, # all article parameters, optionally with dynamic fields + DynamicField => $DynamicField, # all ticket dynamic field parameters Attachment => $Attachment, # all attachment parameters UserID => 123, UserType => 'Agent' # || 'Customer @@ -1545,7 +1516,8 @@ updates a ticket and creates an article and sets dynamic fields and attachments Data => { TicketID => 123, TicketNumber => 'TN3422332', - ArticleID => 123, # if new article was created + ArticleID => 123, # if new article was created, or + ArticleIDs => [ 123, 456 ], # if multiple articles were created } } @@ -1559,11 +1531,11 @@ updates a ticket and creates an article and sets dynamic fields and attachments sub _TicketUpdate { my ( $Self, %Param ) = @_; - my $TicketID = $Param{TicketID}; - my $Ticket = $Param{Ticket}; - my $Article = $Param{Article}; - my $DynamicFieldList = $Param{DynamicFieldList}; - my $AttachmentList = $Param{AttachmentList}; + my $TicketID = $Param{TicketID}; + my $Ticket = $Param{Ticket}; + my $Articles = $Param{Articles}; + my $TicketDynamicFields = $Param{TicketDynamicFields}; + my $AttachmentList = $Param{AttachmentList}; my $Access = $Self->_CheckUpdatePermissions(%Param); @@ -2066,327 +2038,318 @@ sub _TicketUpdate { } } - my $ArticleID; - if ( IsHashRefWithData($Article) ) { + my @ArticleIDs; + for my $Article (@$Articles) { + if ( IsHashRefWithData($Article) ) { - # set Article From - my $From; + # set Article From + my $From; - # When we are sending the article as an email, set the from address to the ticket's system address - if ( - $Article->{ArticleSend} - && !$Article->{From} - ) - { - my $QueueID = $TicketObject->TicketQueueID( - TicketID => $TicketID, - ); - my %Address = $Kernel::OM->Get("Kernel::System::Queue")->GetSystemAddress( - QueueID => $QueueID, - ); - $From = $Address{RealName} . " <" . $Address{Email} . ">"; - } - elsif ( $Article->{From} ) { - $From = $Article->{From}; - } - elsif ( $Param{UserType} eq 'Customer' ) { - - # use data from customer user (if customer user is in database) - if ( IsHashRefWithData( \%CustomerUserData ) ) { - $From = '"' - . $CustomerUserData{UserFullname} . '"' - . ' <' . $CustomerUserData{UserEmail} . '>'; + # When we are sending the article as an email, set the from address to the ticket's system address + if ( + $Article->{ArticleSend} + && !$Article->{From} + ) + { + my $QueueID = $TicketObject->TicketQueueID( + TicketID => $TicketID, + ); + my %Address = $Kernel::OM->Get("Kernel::System::Queue")->GetSystemAddress( + QueueID => $QueueID, + ); + $From = $Address{RealName} . " <" . $Address{Email} . ">"; } - - # otherwise use customer user as sent from the request (it should be an email) - else { - $From = $Ticket->{CustomerUser}; + elsif ( $Article->{From} ) { + $From = $Article->{From}; } - } - else { - my %UserData = $Kernel::OM->Get('Kernel::System::User')->GetUserData( - UserID => $Param{UserID}, - ); - $From = $UserData{UserFullname}; - } - - # Set Article To, Cc, Bcc. - my ( $To, $Cc, $Bcc ); - if ( $Article->{To} ) { - $To = $Article->{To}; - } - if ( $Article->{Cc} ) { - $Cc = $Article->{Cc}; - } - if ( $Article->{Bcc} ) { - $Bcc = $Article->{Bcc}; - } - - # ArticleSend() is only possible for channel 'Email', so set it. - if ( $Article->{ArticleSend} ) { - $Article->{CommunicationChannel} = 'Email'; - } + elsif ( $Param{UserType} eq 'Customer' ) { - # Fallback for To - if ( !$To && $Article->{CommunicationChannel} eq 'Email' ) { + # use data from customer user (if customer user is in database) + if ( IsHashRefWithData( \%CustomerUserData ) ) { + $From = '"' + . $CustomerUserData{UserFullname} . '"' + . ' <' . $CustomerUserData{UserEmail} . '>'; + } - # Use data from customer user (if customer user is in database). - if ( IsHashRefWithData( \%CustomerUserData ) ) { - $To = '"' . $CustomerUserData{UserFullname} . '"' - . ' <' . $CustomerUserData{UserEmail} . '>'; + # otherwise use customer user as sent from the request (it should be an email) + else { + $From = $Ticket->{CustomerUser}; + } } - - # Otherwise use customer user as sent from the request (it should be an email). else { - $To = $Ticket->{CustomerUser} // $TicketData{CustomerUserID}; + my %UserData = $Kernel::OM->Get('Kernel::System::User')->GetUserData( + UserID => $Param{UserID}, + ); + $From = $UserData{UserFullname}; } - } - if ( !$Article->{CommunicationChannel} ) { + # Set Article To, Cc, Bcc. + my ( $To, $Cc, $Bcc ); + $To = $Article->{To} if $Article->{To}; + $Cc = $Article->{Cc} if $Article->{Cc}; + $Bcc = $Article->{Bcc} if $Article->{Bcc}; - my %CommunicationChannel = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelGet( - ChannelID => $Article->{CommunicationChannelID}, - ); - $Article->{CommunicationChannel} = $CommunicationChannel{ChannelName}; - } - - my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForChannel( - ChannelName => $Article->{CommunicationChannel}, - ); + # ArticleSend() is only possible for channel 'Email', so set it. + if ( $Article->{ArticleSend} ) { + $Article->{CommunicationChannel} = 'Email'; + } - my $PlainBody = $Article->{Body}; + # Fallback for To + if ( !$To && $Article->{CommunicationChannel} eq 'Email' ) { - my $ArticleIsHTML = ( $Article->{ContentType} && $Article->{ContentType} =~ /text\/html/i ) - || ( $Article->{MimeType} && $Article->{MimeType} =~ /text\/html/i ); + # Use data from customer user (if customer user is in database). + if ( IsHashRefWithData( \%CustomerUserData ) ) { + $To = '"' . $CustomerUserData{UserFullname} . '"' + . ' <' . $CustomerUserData{UserEmail} . '>'; + } - my $ArticleIsPlainText = ( $Article->{ContentType} && $Article->{ContentType} =~ /text\/plain/i ) - || ( $Article->{MimeType} && $Article->{MimeType} =~ /text\/plain/i ); + # Otherwise use customer user as sent from the request (it should be an email). + else { + $To = $Ticket->{CustomerUser} // $TicketData{CustomerUserID}; + } + } - # Convert article body to plain text, if HTML content was supplied. This is necessary since auto response code - # expects plain text content. Please see bug#13397 for more information. - if ($ArticleIsHTML) { - $PlainBody = $Kernel::OM->Get('Kernel::System::HTMLUtils')->ToAscii( - String => $Article->{Body}, - ); - } + if ( !$Article->{CommunicationChannel} ) { - # Create article. - my $Subject = $Article->{Subject}; - if ( $Article->{ArticleSend} ) { + my %CommunicationChannel = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelGet( + ChannelID => $Article->{CommunicationChannelID}, + ); + $Article->{CommunicationChannel} = $CommunicationChannel{ChannelName}; + } - my $TicketNumber = $TicketObject->TicketNumberLookup( - TicketID => $TicketID, - UserID => $Param{UserID}, + my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForChannel( + ChannelName => $Article->{CommunicationChannel}, ); - # Build a subject - $Subject = $TicketObject->TicketSubjectBuild( - TicketNumber => $TicketNumber, - Subject => $Article->{Subject}, - Type => 'New', - Action => 'Reply', - ); + my $PlainBody = $Article->{Body}; - if ( !$Subject ) { - return { - Success => 0, - ErrorMessage => - 'The subject for the e-mail could not be generated. Please contact the system administrator' - }; - } - - # - # Template generator implicitly takes Frontend::RichText into account. - # Temporarily enable/disable RichText setting according to content type of article, - # so that body and signature both are plain text or HTML. - # - my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); + my $ArticleIsHTML = ( $Article->{ContentType} && $Article->{ContentType} =~ /text\/html/i ) + || ( $Article->{MimeType} && $Article->{MimeType} =~ /text\/html/i ); - my $OriginalRichTextSetting = $ConfigObject->Get('Frontend::RichText'); + my $ArticleIsPlainText = ( $Article->{ContentType} && $Article->{ContentType} =~ /text\/plain/i ) + || ( $Article->{MimeType} && $Article->{MimeType} =~ /text\/plain/i ); - $ConfigObject->{'Frontend::RichText'} = 0 if $ArticleIsPlainText; - $ConfigObject->{'Frontend::RichText'} = 1 if $ArticleIsHTML; + # Convert article body to plain text, if HTML content was supplied. This is necessary since auto response code + # expects plain text content. Please see bug#13397 for more information. + if ($ArticleIsHTML) { + $PlainBody = $Kernel::OM->Get('Kernel::System::HTMLUtils')->ToAscii( + String => $Article->{Body}, + ); + } - my $Signature = $Kernel::OM->Get('Kernel::System::TemplateGenerator')->Signature( - TicketID => $TicketID, - UserID => $Param{UserID}, - Data => $Article, - ); + # Create article. + my $Subject = $Article->{Subject}; + if ( $Article->{ArticleSend} ) { - # Restore original RichText setting. - $ConfigObject->{'Frontend::RichText'} = $OriginalRichTextSetting; + my $TicketNumber = $TicketObject->TicketNumberLookup( + TicketID => $TicketID, + UserID => $Param{UserID}, + ); - if ($Signature) { - $Article->{Body} = $Article->{Body} . $Signature; + # Build a subject + $Subject = $TicketObject->TicketSubjectBuild( + TicketNumber => $TicketNumber, + Subject => $Article->{Subject}, + Type => 'New', + Action => 'Reply', + ); - if ($ArticleIsHTML) { - $PlainBody = $Kernel::OM->Get('Kernel::System::HTMLUtils')->ToAscii( - String => $Article->{Body}, - ); + if ( !$Subject ) { + return { + Success => 0, + ErrorMessage => + 'The subject for the e-mail could not be generated. Please contact the system administrator' + }; } - } - } - # Build Charset if needed (ArticleSend doesn't accept ContentType) - my $Charset; - if ( - $Article->{ContentType} - && !$Article->{Charset} - && $Article->{ContentType} =~ m{\bcharset=("|'|)([^\s"';]+)}ism - ) - { - $Charset = $2; - } - else { - $Charset = $Article->{Charset}; - } + # + # Template generator implicitly takes Frontend::RichText into account. + # Temporarily enable/disable RichText setting according to content type of article, + # so that body and signature both are plain text or HTML. + # + my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); - # Build MimeType if needed (ArticleSend doesn't accept ContentType) - my $MimeType; - if ( - $Article->{ContentType} - && !$Article->{MimeType} - && $Article->{ContentType} =~ m{\A([^;]+)}sm - ) - { - $MimeType = $1; - } - else { - $MimeType = $Article->{MimeType}; - } + my $OriginalRichTextSetting = $ConfigObject->Get('Frontend::RichText'); - # Base-64-decode attachments. - if ( IsHashRefWithData( $Article->{Attachment} ) ) { - $Article->{Attachment} = [ $Article->{Attachment} ]; - } - ATTACHMENT: - for my $Attachment ( @{ $Article->{Attachment} // [] } ) { - next ATTACHMENT if !IsStringWithData( $Attachment->{Content} ); - - $Attachment->{Content} = MIME::Base64::decode_base64( $Attachment->{Content} ); - } - - my %ArticleParams = ( - NoAgentNotify => $Article->{NoAgentNotify} || 0, - TicketID => $TicketID, - SenderTypeID => $Article->{SenderTypeID} || '', - SenderType => $Article->{SenderType} || '', - IsVisibleForCustomer => $Article->{IsVisibleForCustomer}, - From => $From, - To => $To, - Cc => $Cc, - Bcc => $Bcc, - Subject => $Subject, - Body => $Article->{Body}, - MimeType => $MimeType || '', - Charset => $Charset || '', - ContentType => $Article->{ContentType} || '', - UserID => $Param{UserID}, - HistoryType => $Article->{HistoryType}, - HistoryComment => $Article->{HistoryComment} || '%%', - AutoResponseType => $Article->{AutoResponseType}, - UnlockOnAway => $UnlockOnAway, - OrigHeader => { - From => $From, - To => $To, - Subject => $Subject, - Body => $PlainBody - }, - Attachment => $Article->{Attachment} // [], - ); + $ConfigObject->{'Frontend::RichText'} = 0 if $ArticleIsPlainText; + $ConfigObject->{'Frontend::RichText'} = 1 if $ArticleIsHTML; - # create article - if ( $Article->{ArticleSend} ) { + my $Signature = $Kernel::OM->Get('Kernel::System::TemplateGenerator')->Signature( + TicketID => $TicketID, + UserID => $Param{UserID}, + Data => $Article, + ); - # decode and set attachments - if ( IsArrayRefWithData($AttachmentList) ) { + # Restore original RichText setting. + $ConfigObject->{'Frontend::RichText'} = $OriginalRichTextSetting; - my @NewAttachments; - for my $Attachment ( @{$AttachmentList} ) { + if ($Signature) { + $Article->{Body} = $Article->{Body} . $Signature; - push @NewAttachments, { - %{$Attachment}, - Content => MIME::Base64::decode_base64( $Attachment->{Content} ), - }; + if ($ArticleIsHTML) { + $PlainBody = $Kernel::OM->Get('Kernel::System::HTMLUtils')->ToAscii( + String => $Article->{Body}, + ); + } } + } - push @{ $ArticleParams{Attachment} }, @NewAttachments; + # Build Charset if needed (ArticleSend doesn't accept ContentType) + my $Charset; + if ( + $Article->{ContentType} + && !$Article->{Charset} + && $Article->{ContentType} =~ m{\bcharset=("|'|)([^\s"';]+)}ism + ) + { + $Charset = $2; + } + else { + $Charset = $Article->{Charset}; } - # signing and encryption - for my $Key (qw( Sign Crypt )) { - if ( IsHashRefWithData( $Article->{$Key} ) ) { - $ArticleParams{$Key} = $Article->{$Key}; - } + # Build MimeType if needed (ArticleSend doesn't accept ContentType) + my $MimeType; + if ( + $Article->{ContentType} + && !$Article->{MimeType} + && $Article->{ContentType} =~ m{\A([^;]+)}sm + ) + { + $MimeType = $1; + } + else { + $MimeType = $Article->{MimeType}; } - $ArticleID = $ArticleBackendObject->ArticleSend(%ArticleParams); - } - else { - $ArticleID = $ArticleBackendObject->ArticleCreate(%ArticleParams); + # Base-64-decode attachments. + if ( IsHashRefWithData( $Article->{Attachment} ) ) { + $Article->{Attachment} = [ $Article->{Attachment} ]; + } + ATTACHMENT: + for my $Attachment ( @{ $Article->{Attachment} // [] } ) { + next ATTACHMENT if !IsStringWithData( $Attachment->{Content} ); - # set attachments - if ( IsArrayRefWithData($AttachmentList) ) { + $Attachment->{Content} = MIME::Base64::decode_base64( $Attachment->{Content} ); + } - for my $Attachment ( @{$AttachmentList} ) { - my $Result = $Self->CreateAttachment( - TicketID => $TicketID, - Attachment => $Attachment, - ArticleID => $ArticleID, - UserID => $Param{UserID} - ); + my %ArticleParams = ( + NoAgentNotify => $Article->{NoAgentNotify} || 0, + TicketID => $TicketID, + SenderTypeID => $Article->{SenderTypeID} || '', + SenderType => $Article->{SenderType} || '', + IsVisibleForCustomer => $Article->{IsVisibleForCustomer}, + From => $From, + To => $To, + Cc => $Cc, + Bcc => $Bcc, + Subject => $Subject, + Body => $Article->{Body}, + MimeType => $MimeType || '', + Charset => $Charset || '', + ContentType => $Article->{ContentType} || '', + UserID => $Param{UserID}, + HistoryType => $Article->{HistoryType}, + HistoryComment => $Article->{HistoryComment} || '%%', + AutoResponseType => $Article->{AutoResponseType}, + UnlockOnAway => $UnlockOnAway, + OrigHeader => { + From => $From, + To => $To, + Subject => $Subject, + Body => $PlainBody + }, + Attachment => $Article->{Attachment} // [], + ); - if ( !$Result->{Success} ) { - my $ErrorMessage = - $Result->{ErrorMessage} || "Attachment could not be created, please contact" - . " the system administrator"; + # create article + my $ArticleID; + if ( $Article->{ArticleSend} ) { - return { - Success => 0, - ErrorMessage => $ErrorMessage, + # decode and add attachments to first article only + if ( IsArrayRefWithData($AttachmentList) ) { + for my $Attachment ( @{$AttachmentList} ) { + push @{ $ArticleParams{Attachment} }, { + %{$Attachment}, + Content => MIME::Base64::decode_base64( $Attachment->{Content} ), }; } + undef $AttachmentList; # Make sure we only do this once + } + + # signing and encryption + for my $Key (qw( Sign Crypt )) { + if ( IsHashRefWithData( $Article->{$Key} ) ) { + $ArticleParams{$Key} = $Article->{$Key}; + } + } + + $ArticleID = $ArticleBackendObject->ArticleSend(%ArticleParams); + } + else { + $ArticleID = $ArticleBackendObject->ArticleCreate(%ArticleParams); + + # set attachments + if ( IsArrayRefWithData($AttachmentList) ) { + + for my $Attachment ( @{$AttachmentList} ) { + my $Result = $Self->CreateAttachment( + TicketID => $TicketID, + Attachment => $Attachment, + ArticleID => $ArticleID, + UserID => $Param{UserID} + ); + + if ( !$Result->{Success} ) { + my $ErrorMessage = + $Result->{ErrorMessage} || "Attachment could not be created, please contact" + . " the system administrator"; + + return { + Success => 0, + ErrorMessage => $ErrorMessage, + }; + } + } + undef $AttachmentList; # Make sure we only do this once } } - } - if ( !$ArticleID ) { - return { - Success => 0, - ErrorMessage => - 'Article could not be created, please contact the system administrator' - }; - } + if ( !$ArticleID ) { + return { + Success => 0, + ErrorMessage => + 'Article could not be created, please contact the system administrator' + }; + } - # time accounting - if ( $Article->{TimeUnit} ) { - $TicketObject->TicketAccountTime( - TicketID => $TicketID, - ArticleID => $ArticleID, - TimeUnit => $Article->{TimeUnit}, - UserID => $Param{UserID}, - ); + if ( $Article->{DynamicField} ) { + my $ArticleDynamicFieldList = _MakeArrayRef( $Article->{DynamicField} ); + if ( my $Result + = $Self->_SetDynamicFields( $ArticleDynamicFieldList, $TicketID, $ArticleID, $Param{UserID} ) ) + { + return $Result; + } + } + + # time accounting + if ( $Article->{TimeUnit} ) { + $TicketObject->TicketAccountTime( + TicketID => $TicketID, + ArticleID => $ArticleID, + TimeUnit => $Article->{TimeUnit}, + UserID => $Param{UserID}, + ); + } + push @ArticleIDs, $ArticleID; } } - # set dynamic fields - for my $DynamicField ( @{$DynamicFieldList} ) { - my $Result = $Self->SetDynamicFieldValue( - %{$DynamicField}, - TicketID => $TicketID, - ArticleID => $ArticleID || '', - UserID => $Param{UserID}, - ); - - if ( !$Result->{Success} ) { - my $ErrorMessage = - $Result->{ErrorMessage} || "Dynamic Field $DynamicField->{Name} could not be set," - . " please contact the system administrator"; - - return { - Success => 0, - ErrorMessage => $ErrorMessage, - }; + # Set all dynamic fields if specified, on the first article if present, ticket otherwise + if ( $TicketDynamicFields && @$TicketDynamicFields ) { + if ( my $Result = $Self->_SetDynamicFields( $TicketDynamicFields, $TicketID, $ArticleIDs[0], $Param{UserID} ) ) + { + return $Result; } } @@ -2403,24 +2366,15 @@ sub _TicketUpdate { $IncludeTicketData = $OperationConfig->{IncludeTicketData}; } + # We're done here unless IncludeTicketData was set if ( !$IncludeTicketData ) { - if ($ArticleID) { - return { - Success => 1, - Data => { - TicketID => $TicketID, - TicketNumber => $TicketData{TicketNumber}, - ArticleID => $ArticleID, - }, - }; - } - return { - Success => 1, - Data => { + return $Self->_ReturnSuccess( + { TicketID => $TicketID, TicketNumber => $TicketData{TicketNumber}, }, - }; + \@ArticleIDs, + ); } # get updated TicketData @@ -2435,13 +2389,12 @@ sub _TicketUpdate { my %TicketDynamicFields; TICKETATTRIBUTE: for my $TicketAttribute ( sort keys %TicketData ) { - if ( $TicketAttribute =~ m{\A DynamicField_(.*) \z}msx ) { - $TicketDynamicFields{$1} = { - Name => $1, - Value => $TicketData{$TicketAttribute}, - }; - delete $TicketData{$TicketAttribute}; - } + next TICKETATTRIBUTE if $TicketAttribute !~ m{\A DynamicField_(.*) \z}msx; + $TicketDynamicFields{$1} = { + Name => $1, + Value => $TicketData{$TicketAttribute}, + }; + delete $TicketData{$TicketAttribute}; } # add dynamic fields as array into 'DynamicField' hash key if any @@ -2449,22 +2402,8 @@ sub _TicketUpdate { $TicketData{DynamicField} = [ sort { $a->{Name} cmp $b->{Name} } values %TicketDynamicFields ]; } - # get last ArticleID - my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); - my $ArticleBackendObject = $ArticleObject->BackendForArticle( - ArticleID => $ArticleID, - TicketID => $TicketID - ); - - my @Articles = $ArticleObject->ArticleList( - TicketID => $TicketID, - OnlyLast => 1, - ); - - my $LastArticleID = $Articles[0]->{ArticleID}; - # return ticket data if we have no article data - if ( !$ArticleID && !$LastArticleID ) { + if ( !@ArticleIDs ) { return { Success => 1, Data => { @@ -2475,40 +2414,44 @@ sub _TicketUpdate { }; } - # get Article and ArticleAttachement - my %ArticleData = $ArticleBackendObject->ArticleGet( - ArticleID => $ArticleID || $LastArticleID, - DynamicFields => 1, - TicketID => $TicketID, - ); + # get affected Articles + my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); + for my $ArticleID (@ArticleIDs) { + my $ArticleBackendObject = $ArticleObject->BackendForArticle( + ArticleID => $ArticleID, + TicketID => $TicketID + ); - # prepare Article DynamicFields - my @ArticleDynamicFields; + # get Article and ArticleAttachement + my %ArticleData = $ArticleBackendObject->ArticleGet( + ArticleID => $ArticleID, + DynamicFields => 1, + TicketID => $TicketID, + ); - # remove all dynamic fields from main ticket hash and set them into an array. - ARTICLEATTRIBUTE: - for my $ArticleAttribute ( sort keys %ArticleData ) { - if ( $ArticleAttribute =~ m{\A DynamicField_(.*) \z}msx ) { + # prepare Article DynamicFields + my @ArticleDynamicFields; + + # remove all dynamic fields from main ticket hash and set them into an array. + ARTICLEATTRIBUTE: + for my $ArticleAttribute ( sort keys %ArticleData ) { + next ARTICLEATTRIBUTE if $ArticleAttribute !~ m{\A DynamicField_(.*) \z}msx; if ( !exists $TicketDynamicFields{$1} ) { push @ArticleDynamicFields, { Name => $1, Value => $ArticleData{$ArticleAttribute}, }; } - delete $ArticleData{$ArticleAttribute}; } - } - # add dynamic fields array into 'DynamicField' hash key if any - if (@ArticleDynamicFields) { - $ArticleData{DynamicField} = \@ArticleDynamicFields; - } + # add dynamic fields array into 'DynamicField' hash key if any + if (@ArticleDynamicFields) { + $ArticleData{DynamicField} = \@ArticleDynamicFields; + } - # add attachment if the request includes attachments - if ( IsArrayRefWithData($AttachmentList) ) { my %AttachmentIndex = $ArticleBackendObject->ArticleAttachmentIndex( - ArticleID => $ArticleData{ArticleID}, + ArticleID => $ArticleID, ); my @Attachments; @@ -2525,26 +2468,75 @@ sub _TicketUpdate { # convert content to base64, but prevent 76 chars brake, see bug#14500. $Attachment{Content} = MIME::Base64::encode_base64( $Attachment{Content}, '' ); - push @Attachments, {%Attachment}; + push @Attachments, \%Attachment; } # set Attachments data if (@Attachments) { $ArticleData{Attachment} = \@Attachments; } - } - $TicketData{Article} = \%ArticleData; + if ( @ArticleIDs == 1 ) { + $TicketData{Article} = \%ArticleData; + } + else { + push @{ $TicketData{Articles} }, \%ArticleData; + } + } # return ticket data and article data - return { - Success => 1, - Data => { + return $Self->_ReturnSuccess( + { TicketID => $TicketID, TicketNumber => $TicketData{TicketNumber}, - ArticleID => $ArticleData{ArticleID}, Ticket => \%TicketData, }, + \@ArticleIDs, + ); +} + +# Set all dynamic fields, return undef if everything is OK, +# otherwise an error hash to propagate up +sub _SetDynamicFields { + my ( $Self, $DynamicFieldList, $TicketID, $ArticleID, $UserID ) = @_; + + for my $DynamicField ( @{$DynamicFieldList} ) { + my $Result = $Self->SetDynamicFieldValue( + %{$DynamicField}, + TicketID => $TicketID, + ArticleID => $ArticleID, + UserID => $UserID, + ); + + if ( !$Result->{Success} ) { + my $ErrorMessage = + $Result->{ErrorMessage} || "Dynamic Field $DynamicField->{Name} could not be set," + . " please contact the system administrator"; + + return { + Success => 0, + ErrorMessage => $ErrorMessage, + }; + } + } + return; +} + +# Return success, augmenting $ReturnData either with "ArticleID" or "ArticleIDs" +# depending on the length of the @$ArticleIDs array +sub _ReturnSuccess { + my ( $Self, $ReturnData, $ArticleIDs ) = @_; + + if ( @$ArticleIDs > 1 ) { + $ReturnData->{ArticleIDs} = $ArticleIDs; + } + elsif ( @$ArticleIDs == 1 ) { + $ReturnData->{ArticleID} = $ArticleIDs->[0]; + } + + return { + Success => 1, + Data => $ReturnData, }; } diff --git a/scripts/test/GenericInterface/Operation/Ticket/Common.t b/scripts/test/GenericInterface/Operation/Ticket/Common.t index 38ac9adcc55..cbef56ccf03 100644 --- a/scripts/test/GenericInterface/Operation/Ticket/Common.t +++ b/scripts/test/GenericInterface/Operation/Ticket/Common.t @@ -750,7 +750,7 @@ my @Tests = ( UserID => 1, }, ExpectedData => { - ErrorMessage => 'SetDynamicFieldValue() Could not set !', + ErrorMessage => 'SetDynamicFieldValue() Could not set ObjectID!', Success => 0, }, }, diff --git a/scripts/test/GenericInterface/Operation/Ticket/TicketUpdate.t b/scripts/test/GenericInterface/Operation/Ticket/TicketUpdate.t index e05f0b9c5b6..6104bb6d829 100644 --- a/scripts/test/GenericInterface/Operation/Ticket/TicketUpdate.t +++ b/scripts/test/GenericInterface/Operation/Ticket/TicketUpdate.t @@ -20,7 +20,6 @@ use Kernel::GenericInterface::Requester; use Kernel::System::VariableCheck qw(:all); -# get helper object # skip SSL certificate verification $Kernel::OM->ObjectParamAdd( 'Kernel::System::UnitTest::Helper' => { @@ -36,7 +35,6 @@ $HelperObject->ConfigSettingChange( Value => 0, ); -# get a random number my $RandomID = $HelperObject->GetRandomNumber(); # create a new user for current test @@ -45,7 +43,6 @@ my $UserLogin = $HelperObject->TestUserCreate( ); my $Password = $UserLogin; -# new user object my $UserObject = $Kernel::OM->Get('Kernel::System::User'); $Self->{UserID} = $UserObject->UserLookup( @@ -64,7 +61,6 @@ my $CustomerPassword = $CustomerUserLogin; my $CustomerUserLogin2 = $HelperObject->TestCustomerUserCreate(); my $CustomerPassword2 = $CustomerUserLogin2; -# create dynamic field object my $DynamicFieldObject = $Kernel::OM->Get('Kernel::System::DynamicField'); # add text dynamic field @@ -970,18 +966,12 @@ for my $Test (@Tests) { if ( $Test->{RequestData}->{Article} ) { # Get latest article data. - my @ArticleList = $ArticleObject->ArticleList( + my ($Article) = $ArticleObject->ArticleList( TicketID => $TicketID1, OnlyLast => 1, ); - my $ArticleID; - - ARTICLE: - for my $Article (@ArticleList) { - $ArticleID = $Article->{ArticleID}; - last ARTICLE; - } + my $ArticleID = $Article->{ArticleID}; if ( $Test->{ExpectedReturnLocalData} ) { $Test->{ExpectedReturnLocalData}->{Data} = { @@ -1125,84 +1115,77 @@ for my $Test (@Tests) { } # Get latest article data. - my @ArticleList = $ArticleObject->ArticleList( + my ($LastArticle) = $ArticleObject->ArticleList( TicketID => $TicketID1, OnlyLast => 1, ); my %Article; - ARTICLE: - for my $Article (@ArticleList) { - my $ArticleBackendObject = $ArticleObject->BackendForArticle( %{$Article} ); + my $ArticleBackendObject = $ArticleObject->BackendForArticle( %{$LastArticle} ); - %Article = $ArticleBackendObject->ArticleGet( - %{$Article}, - DynamicFields => 1, - ); + %Article = $ArticleBackendObject->ArticleGet( + %{$LastArticle}, + DynamicFields => 1, + ); - for my $Key ( sort keys %Article ) { - $Article{$Key} //= ''; - } + for my $Key ( sort keys %Article ) { + $Article{$Key} //= ''; + } - # Push all dynamic field data in a separate array structure. - my $DynamicFields; - KEY: - for my $Key ( sort keys %Article ) { - if ( $Key =~ m{^DynamicField_(?\w+)$}xms ) { - push @{$DynamicFields}, { - Name => $+{DFName}, - Value => $Article{$Key} // '', - }; - next KEY; - } - } - for my $DynamicField ( @{$DynamicFields} ) { - delete $Article{"DynamicField_$DynamicField->{Name}"}; - } - if ( scalar @{$DynamicFields} == 1 ) { - $DynamicFields = $DynamicFields->[0]; - } - if ( IsArrayRefWithData($DynamicFields) || IsHashRefWithData($DynamicFields) ) { - $Article{DynamicField} = $DynamicFields; + # Push all dynamic field data in a separate array structure. + my $DynamicFields; + for my $Key ( sort keys %Article ) { + if ( $Key =~ m{^DynamicField_(?\w+)$}xms ) { + push @{$DynamicFields}, { + Name => $+{DFName}, + Value => $Article{$Key} // '', + }; } + } + for my $DynamicField ( @{$DynamicFields} ) { + delete $Article{"DynamicField_$DynamicField->{Name}"}; + } + if ( scalar @{$DynamicFields} == 1 ) { + $DynamicFields = $DynamicFields->[0]; + } + if ( IsArrayRefWithData($DynamicFields) || IsHashRefWithData($DynamicFields) ) { + $Article{DynamicField} = $DynamicFields; + } + + my %AttachmentIndex = $ArticleBackendObject->ArticleAttachmentIndex( + ArticleID => $Article{ArticleID}, + ); - my %AttachmentIndex = $ArticleBackendObject->ArticleAttachmentIndex( + my @Attachments; + $Kernel::OM->Get('Kernel::System::Main')->Require('MIME::Base64'); + ATTACHMENT: + for my $FileID ( sort keys %AttachmentIndex ) { + next ATTACHMENT if !$FileID; + my %Attachment = $ArticleBackendObject->ArticleAttachment( ArticleID => $Article{ArticleID}, + FileID => $FileID, ); - my @Attachments; - $Kernel::OM->Get('Kernel::System::Main')->Require('MIME::Base64'); - ATTACHMENT: - for my $FileID ( sort keys %AttachmentIndex ) { - next ATTACHMENT if !$FileID; - my %Attachment = $ArticleBackendObject->ArticleAttachment( - ArticleID => $Article{ArticleID}, - FileID => $FileID, - ); - - next ATTACHMENT if !IsHashRefWithData( \%Attachment ); + next ATTACHMENT if !IsHashRefWithData( \%Attachment ); - # Convert content to base64. - $Attachment{Content} = MIME::Base64::encode_base64( $Attachment{Content}, '' ); - push @Attachments, {%Attachment}; - } + # Convert content to base64. + $Attachment{Content} = MIME::Base64::encode_base64( $Attachment{Content}, '' ); + push @Attachments, {%Attachment}; + } - # Set attachment data. - if (@Attachments) { + # Set attachment data. + if (@Attachments) { - # Flatten array if only one attachment was found. - if ( scalar @Attachments == 1 ) { - for my $Attachment (@Attachments) { - $Article{Attachment} = $Attachment; - } - } - else { - $Article{Attachment} = \@Attachments; + # Flatten array if only one attachment was found. + if ( scalar @Attachments == 1 ) { + for my $Attachment (@Attachments) { + $Article{Attachment} = $Attachment; } } - - last ARTICLE; + else { + $Article{Attachment} = \@Attachments; + } } # Transform some article properties so they match expected data structure. diff --git a/scripts/test/GenericInterface/Operation/Ticket/TicketUpdateMultiple.t b/scripts/test/GenericInterface/Operation/Ticket/TicketUpdateMultiple.t new file mode 100644 index 00000000000..4649f806e5f --- /dev/null +++ b/scripts/test/GenericInterface/Operation/Ticket/TicketUpdateMultiple.t @@ -0,0 +1,954 @@ +# -- +# Copyright (C) 2001-2021 OTRS AG, https://otrs.com/ +# Copyright (C) 2021 Znuny GmbH, https://znuny.org/ +# -- +# This software comes with ABSOLUTELY NO WARRANTY. For details, see +# the enclosed file COPYING for license information (GPL). If you +# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. +# -- + +## no critic (Modules::RequireExplicitPackage) +package main; +use strict; +use warnings; +use utf8; +use Storable 'dclone'; +use vars (qw($Self)); + +use Kernel::GenericInterface::Debugger; +use Kernel::GenericInterface::Operation::Session::SessionCreate; +use Kernel::GenericInterface::Operation::Ticket::TicketUpdate; +use Kernel::GenericInterface::Requester; + +use Kernel::System::VariableCheck qw(:all); + +# skip SSL certificate verification +$Kernel::OM->ObjectParamAdd( + 'Kernel::System::UnitTest::Helper' => { + SkipSSLVerify => 1, + }, +); +my $HelperObject = $Kernel::OM->Get('Kernel::System::UnitTest::Helper'); + +$HelperObject->ConfigSettingChange( + Valid => 1, + Key => 'CheckMXRecord', + Value => 0, +); + +my $RandomID = $HelperObject->GetRandomNumber(); + +# create a new user for current test +my $UserLogin = $HelperObject->TestUserCreate( + Groups => ['users'], +); +my $Password = $UserLogin; + +my $UserObject = $Kernel::OM->Get('Kernel::System::User'); + +$Self->{UserID} = $UserObject->UserLookup( + UserLogin => $UserLogin, +); + +# create a new user without permissions for current test +my $UserLogin2 = $HelperObject->TestUserCreate(); +my $Password2 = $UserLogin2; + +# create a customer where a ticket will use and will have permissions +my $CustomerUserLogin = $HelperObject->TestCustomerUserCreate(); +my $CustomerPassword = $CustomerUserLogin; + +# create a customer that will not have permissions +my $CustomerUserLogin2 = $HelperObject->TestCustomerUserCreate(); +my $CustomerPassword2 = $CustomerUserLogin2; + +my $DynamicFieldObject = $Kernel::OM->Get('Kernel::System::DynamicField'); + +# add text dynamic field +my %DynamicFieldTextConfig = ( + Name => "Unittest1$RandomID", + FieldOrder => 9991, + FieldType => 'Text', + ObjectType => 'Ticket', + Label => 'Description', + ValidID => 1, + Config => { + DefaultValue => '', + }, +); +my $FieldTextID = $DynamicFieldObject->DynamicFieldAdd( + %DynamicFieldTextConfig, + UserID => 1, + Reorder => 0, +); +$Self->True( + $FieldTextID, + "Dynamic Field $FieldTextID", +); + +# add ID +$DynamicFieldTextConfig{ID} = $FieldTextID; + +# add dropdown dynamic field +my %DynamicFieldDropdownConfig = ( + Name => "Unittest2$RandomID", + FieldOrder => 9992, + FieldType => 'Dropdown', + ObjectType => 'Ticket', + Label => 'Description', + ValidID => 1, + Config => { + PossibleValues => { + 1 => 'One', + 2 => 'Two', + 3 => 'Three', + 0 => '0', + }, + }, +); +my $FieldDropdownID = $DynamicFieldObject->DynamicFieldAdd( + %DynamicFieldDropdownConfig, + UserID => 1, + Reorder => 0, +); +$Self->True( + $FieldDropdownID, + "Dynamic Field $FieldDropdownID", +); + +# add ID +$DynamicFieldDropdownConfig{ID} = $FieldDropdownID; + +# add multiselect dynamic field +my %DynamicFieldMultiselectConfig = ( + Name => "Unittest3$RandomID", + FieldOrder => 9993, + FieldType => 'Multiselect', + ObjectType => 'Ticket', + Label => 'Multiselect label', + ValidID => 1, + Config => { + PossibleValues => { + 1 => 'Value9ßüß', + 2 => 'DifferentValue', + 3 => '1234567', + }, + }, +); +my $FieldMultiselectID = $DynamicFieldObject->DynamicFieldAdd( + %DynamicFieldMultiselectConfig, + UserID => 1, + Reorder => 0, +); +$Self->True( + $FieldMultiselectID, + "Dynamic Field $FieldMultiselectID", +); + +# add ID +$DynamicFieldMultiselectConfig{ID} = $FieldMultiselectID; + +# create ticket object +my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket'); + +#ticket id container +my @TicketIDs; + +# create ticket 1 +my $TicketID1 = $TicketObject->TicketCreate( + Title => 'Ticket One Title', + Queue => 'Raw', + Lock => 'unlock', + Priority => '3 normal', + State => 'new', + CustomerID => $CustomerUserLogin, + CustomerUser => 'unittest@otrs.com', + OwnerID => 1, + UserID => 1, +); + +# sanity check +$Self->True( + $TicketID1, + "TicketCreate() successful for Ticket One ID $TicketID1", +); + +my %Ticket = $TicketObject->TicketGet( + TicketID => $TicketID1, + UserID => 1, +); + +# remember ticket id +push @TicketIDs, $TicketID1; + +# create backed object +my $BackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); +$Self->Is( + ref $BackendObject, + 'Kernel::System::DynamicField::Backend', + 'Backend object was created successfully', +); + +# set text field value +my $Result = $BackendObject->ValueSet( + DynamicFieldConfig => \%DynamicFieldTextConfig, + ObjectID => $TicketID1, + Value => 'ticket1_field1', + UserID => 1, +); + +# sanity check +$Self->True( + $Result, + "Text ValueSet() for Ticket $TicketID1", +); + +# set dropdown field value +$Result = $BackendObject->ValueSet( + DynamicFieldConfig => \%DynamicFieldDropdownConfig, + ObjectID => $TicketID1, + Value => 1, + UserID => 1, +); + +# sanity check +$Self->True( + $Result, + "Multiselect ValueSet() for Ticket $TicketID1", +); + +# set multiselect field value +$Result = $BackendObject->ValueSet( + DynamicFieldConfig => \%DynamicFieldMultiselectConfig, + ObjectID => $TicketID1, + Value => [ 2, 3 ], + UserID => 1, +); + +# sanity check +$Self->True( + $Result, + "Dropdown ValueSet() for Ticket $TicketID1", +); + +# set web service name +my $WebserviceName = $HelperObject->GetRandomID(); + +# create web-service object +my $WebserviceObject = $Kernel::OM->Get('Kernel::System::GenericInterface::Webservice'); + +$Self->Is( + 'Kernel::System::GenericInterface::Webservice', + ref $WebserviceObject, + "Create web service object", +); + +my $WebserviceID = $WebserviceObject->WebserviceAdd( + Name => $WebserviceName, + Config => { + Debugger => { + DebugThreshold => 'debug', + }, + Provider => { + Transport => { + Type => '', + }, + }, + }, + ValidID => 1, + UserID => 1, +); +$Self->True( + $WebserviceID, + "Added web service", +); + +# get config object +my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); + +# get remote host with some precautions for certain unit test systems +my $Host = $HelperObject->GetTestHTTPHostname(); + +# prepare web-service config +my $RemoteSystem = + $ConfigObject->Get('HttpType') + . '://' + . $Host + . '/' + . $ConfigObject->Get('ScriptAlias') + . '/nph-genericinterface.pl/WebserviceID/' + . $WebserviceID; + +my $WebserviceConfig = { + + # Name => '', + Description => + 'Test for Ticket Connector using SOAP transport backend.', + Debugger => { + DebugThreshold => 'debug', + TestMode => 1, + }, + Provider => { + Transport => { + Type => 'HTTP::SOAP', + Config => { + MaxLength => 10000000, + NameSpace => 'http://otrs.org/SoapTestInterface/', + Endpoint => $RemoteSystem, + }, + }, + Operation => { + TicketUpdate => { + Type => 'Ticket::TicketUpdate', + }, + SessionCreate => { + Type => 'Session::SessionCreate', + }, + }, + }, + Requester => { + Transport => { + Type => 'HTTP::SOAP', + Config => { + NameSpace => 'http://otrs.org/SoapTestInterface/', + Encoding => 'UTF-8', + Endpoint => $RemoteSystem, + Timeout => 120, + }, + }, + Invoker => { + TicketUpdate => { + Type => 'Test::TestSimple', + }, + SessionCreate => { + Type => 'Test::TestSimple', + }, + }, + }, +}; + +# update web-service with real config +# the update is needed because we are using +# the WebserviceID for the Endpoint in config +my $WebserviceUpdate = $WebserviceObject->WebserviceUpdate( + ID => $WebserviceID, + Name => $WebserviceName, + Config => $WebserviceConfig, + ValidID => 1, + UserID => $Self->{UserID}, +); +$Self->True( + $WebserviceUpdate, + "Updated web service $WebserviceID - $WebserviceName", +); + +# disable SessionCheckRemoteIP setting +$ConfigObject->Set( + Key => 'SessionCheckRemoteIP', + Value => 0, +); + +# Get SessionID +# create requester object +my $RequesterSessionObject = $Kernel::OM->Get('Kernel::GenericInterface::Requester'); + +$Self->Is( + 'Kernel::GenericInterface::Requester', + ref $RequesterSessionObject, + "SessionID - Create requester object", +); + +# start requester with our web-service +my $RequesterSessionResult = $RequesterSessionObject->Run( + WebserviceID => $WebserviceID, + Invoker => 'SessionCreate', + Data => { + UserLogin => $UserLogin, + Password => $Password, + }, +); + +my $NewSessionID = $RequesterSessionResult->{Data}->{SessionID}; + +my @Tests = ( + { + Name => 'Add articles with attachment', + SuccessRequest => '1', + RequestData => { + TicketID => $TicketID1, + Ticket => { + Title => 'Updated', + }, + Article => [ + { + Subject => 'Article subject äöüßÄÖÜ€ис', + Body => 'Article body', + AutoResponseType => 'auto reply', + IsVisibleForCustomer => 1, + CommunicationChannel => 'Email', + SenderType => 'agent', + From => 'enjoy@otrs.com', + Charset => 'utf8', + MimeType => 'text/plain', + HistoryType => 'AddNote', + HistoryComment => '%%', + }, + { + Subject => 'Article2 subject ຟູບາຣ', + Body => 'Article2 body', + IsVisibleForCustomer => 1, + CommunicationChannel => 'Phone', + SenderType => 'agent', + From => 'foo@otrs.com', + Charset => 'latin-1', + MimeType => 'text/plain', + HistoryType => 'AddNote', + HistoryComment => '%%', + }, + ], + Attachment => [ + { + Content => 'Ymx1YiBibHViIGJsdWIg', + ContentType => 'text/html', + Filename => 'test.txt', + }, + ], + }, + IncludeTicketData => 1, + ExpectedReturnRemoteData => { + Success => 1, + Data => { + TicketID => $Ticket{TicketID}, + TicketNumber => $Ticket{TicketNumber}, + }, + }, + ExpectedReturnLocalData => { + Success => 1, + Data => { + TicketID => $Ticket{TicketID}, + TicketNumber => $Ticket{TicketNumber}, + }, + }, + Operation => 'TicketUpdate', + }, + { + Name => 'Add email articles with attachment named "0"', + SuccessRequest => '1', + RequestData => { + TicketID => $TicketID1, + Ticket => { + Title => 'Updated', + }, + Article => [ + { + Subject => 'Article1 subject', + Body => 'Article1 body', + AutoResponseType => 'auto reply', + IsVisibleForCustomer => 1, + CommunicationChannel => 'Email', + SenderType => 'agent', + Charset => 'utf8', + MimeType => 'text/plain', + HistoryType => 'AddNote', + HistoryComment => '%%', + }, + { + Subject => 'Article2 subject', + Body => 'Article2 body', + AutoResponseType => 'auto reply', + IsVisibleForCustomer => 1, + CommunicationChannel => 'Email', + SenderType => 'agent', + Charset => 'utf8', + MimeType => 'text/plain', + HistoryType => 'AddNote', + HistoryComment => '%%', + }, + ], + Attachment => [ + { + Content => 'Ymx1YiBibHViIGJsdWIg', + ContentType => 'text/html', + Filename => '0', + }, + ], + }, + IncludeTicketData => 1, + ExpectedReturnRemoteData => { + Success => 1, + Data => { + TicketID => $Ticket{TicketID}, + TicketNumber => $Ticket{TicketNumber}, + }, + }, + ExpectedReturnLocalData => { + Success => 1, + Data => { + TicketID => $Ticket{TicketID}, + TicketNumber => $Ticket{TicketNumber}, + }, + }, + AlsoExpect => { + Remote => sub { + my ( $Self, $Result, $LocalRemote ) = @_; + + $Self->Is( + $Result->{Data}->{Ticket}->{Articles}->[0]->{Attachment}->{Filename}, + '0', + "Attachment found in first article ($LocalRemote)" + ); + $Self->False( + exists $Result->{Data}->{Ticket}->{Articles}->[1]->{Attachment}, + "No attachment found in second article ($LocalRemote)" + ); + }, + }, + Operation => 'TicketUpdate', + }, +); + +# debugger object +my $DebuggerObject = Kernel::GenericInterface::Debugger->new( + %{$Self}, + DebuggerConfig => { + DebugThreshold => 'debug', + TestMode => 1, + }, + WebserviceID => $WebserviceID, + CommunicationType => 'Provider', +); +$Self->Is( + ref $DebuggerObject, + 'Kernel::GenericInterface::Debugger', + 'DebuggerObject instantiate correctly', +); + +$HelperObject->FixedTimeSet(); + +my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); + +TEST: +for my $Test (@Tests) { + + # Update web service config to include ticket data in responses. + if ( $Test->{IncludeTicketData} ) { + $WebserviceConfig->{Provider}->{Operation}->{TicketUpdate}->{IncludeTicketData} = 1; + my $WebserviceUpdate = $WebserviceObject->WebserviceUpdate( + ID => $WebserviceID, + Name => $WebserviceName, + Config => $WebserviceConfig, + ValidID => 1, + UserID => $Self->{UserID}, + ); + $Self->True( + $WebserviceUpdate, + 'WebserviceUpdate - Turned on IncludeTicketData' + ); + } + + # create local object + my $LocalObject = "Kernel::GenericInterface::Operation::Ticket::$Test->{Operation}"->new( + %{$Self}, + DebuggerObject => $DebuggerObject, + WebserviceID => $WebserviceID, + ConfigObject => $ConfigObject, + ); + + $Self->Is( + "Kernel::GenericInterface::Operation::Ticket::$Test->{Operation}", + ref $LocalObject, + "$Test->{Name} - Create local object", + ); + + my %Auth = ( + UserLogin => $UserLogin, + Password => $Password, + ); + if ( IsHashRefWithData( $Test->{Auth} ) ) { + %Auth = %{ $Test->{Auth} }; + } + + # start requester with our web-service + my $LocalResult = $LocalObject->Run( + WebserviceID => $WebserviceID, + Invoker => $Test->{Operation}, + Data => { + %Auth, + %{ $Test->{RequestData} }, + }, + ); + + # check result + $Self->Is( + 'HASH', + ref $LocalResult, + "$Test->{Name} - Local result structure is valid", + ); + + _ExpectLatestArticles( $Test, $TicketID1, 'Local' ); + + # create requester object + my $RequesterObject = Kernel::GenericInterface::Requester->new( + %{$Self}, + ConfigObject => $ConfigObject, + ); + $Self->Is( + 'Kernel::GenericInterface::Requester', + ref $RequesterObject, + "$Test->{Name} - Create requester object", + ); + + # start requester with our web-service + my $RequesterResult = $RequesterObject->Run( + WebserviceID => $WebserviceID, + Invoker => $Test->{Operation}, + Data => { + %Auth, + %{ $Test->{RequestData} }, + }, + ); + + # TODO prevent failing test if environment on SaaS unit test system doesn't work. + if ( + $RequesterResult->{ErrorMessage} eq + 'faultcode: Server, faultstring: Attachment could not be created, please contact the system administrator' + ) + { + next TEST; + } + + # check result + $Self->Is( + 'HASH', + ref $RequesterResult, + "$Test->{Name} - Requester result structure is valid", + ); + + $Self->Is( + $RequesterResult->{Success}, + $Test->{SuccessRequest}, + "$Test->{Name} - Requester successful result", + ); + + # remove ErrorMessage parameter from direct call + # result to be consistent with SOAP call result + if ( $LocalResult->{ErrorMessage} ) { + delete $LocalResult->{ErrorMessage}; + } + + if ( $Test->{IncludeTicketData} ) { + my %TicketGet = $TicketObject->TicketGet( + TicketID => $TicketID1, + DynamicFields => 1, + Extended => 1, + UserID => 1, + ); + + my %TicketData; + my @DynamicFields; + + # Transform some ticket properties so they match expected data structure. + KEY: + for my $Key ( sort keys %TicketGet ) { + + # Quote some properties as strings. + if ( $Key eq 'UntilTime' ) { + $TicketData{$Key} = "$TicketGet{$Key}"; + next KEY; + } + + # Push all dynamic field data in a separate array structure. + elsif ( $Key =~ m{^DynamicField_(?\w+)$}xms ) { + push @DynamicFields, { + Name => $+{DFName}, + Value => $TicketGet{$Key} // '', + }; + next KEY; + } + + # Skip some fields since they might differ. + elsif ( + $Key eq 'Age' + || $Key eq 'FirstResponse' + || $Key eq 'Changed' + || $Key eq 'UnlockTimeout' + ) + { + next KEY; + } + + # Include any other ticket property as-is. Undefined values should be represented as empty string. + $TicketData{$Key} = $TicketGet{$Key} // ''; + } + + if (@DynamicFields) { + $TicketData{DynamicField} = \@DynamicFields; + } + + $Test->{ExpectedReturnRemoteData}->{Data} = { + %{ $Test->{ExpectedReturnRemoteData}->{Data} }, + Ticket => \%TicketData, + }; + } + + if ( defined $Test->{RequestData}->{Ticket}->{CustomerUser} ) { + $Self->Is( + "\"$CustomerUserLogin $CustomerUserLogin\" <$CustomerUserLogin\@localunittest.com>", + $RequesterResult->{Data}->{Ticket}->{Article}->{To}, + "Article parameter To is set well after TicketUpdate()", + ); + } + + $Kernel::OM->Get('Kernel::System::Cache')->CleanUp(); + + # Check if parameters To, cc and Bcc set well + # See bug#14393 for more information. + my $RequestArticles = $Test->{RequestData}->{Article}; + my $ResponseArticles = $RequesterResult->{Data}->{Ticket}->{Articles}; + REQUESTARTICLE: + for my $i ( 0 .. $#$RequestArticles ) { + for my $Header (qw(To Cc Bcc)) { + next REQUESTARTICLE if !defined $RequestArticles->[$i]->{$Header}; + $Self->Is( + $RequestArticles->[$i]->{$Header}, + $ResponseArticles->[$i]->{$Header}, + "Article parameter $Header is set well after TicketUpdate() - $$RequestArticles->[ $i ]->{ $Header }", + ); + } + } + + my @ArticleList = _ExpectLatestArticles( $Test, $TicketID1, 'Remote' ); + + my @Articles; + for my $QueryArticle (@ArticleList) { + my %Article; + + my $ArticleBackendObject = $ArticleObject->BackendForArticle( %{$QueryArticle} ); + %Article = $ArticleBackendObject->ArticleGet( + %{$QueryArticle}, + DynamicFields => 1, + ); + + $_ //= '' for values %Article; + + # Push all dynamic field data in a separate array structure. + my $DynamicFields; + for my $Key ( sort grep {m{^DynamicField_}} keys %Article ) { + push @{$DynamicFields}, { + Name => substr( $Key, length('DynamicField_') ), + Value => $Article{$Key} // '', + }; + delete $Article{$Key}; + } + if ( scalar @{$DynamicFields} == 1 ) { + $DynamicFields = $DynamicFields->[0]; + } + if ( IsArrayRefWithData($DynamicFields) || IsHashRefWithData($DynamicFields) ) { + $Article{DynamicField} = $DynamicFields; + } + + my %AttachmentIndex = $ArticleBackendObject->ArticleAttachmentIndex( + ArticleID => $Article{ArticleID}, + ); + + my @Attachments; + $Kernel::OM->Get('Kernel::System::Main')->Require('MIME::Base64'); + ATTACHMENT: + for my $FileID ( sort keys %AttachmentIndex ) { + next ATTACHMENT if !$FileID; + my %Attachment = $ArticleBackendObject->ArticleAttachment( + ArticleID => $Article{ArticleID}, + FileID => $FileID, + ); + + next ATTACHMENT if !IsHashRefWithData( \%Attachment ); + + # Convert content to base64. + $Attachment{Content} = MIME::Base64::encode_base64( $Attachment{Content}, '' ); + push @Attachments, {%Attachment}; + } + + # Set attachment data. + if (@Attachments) { + + # Flatten array if only one attachment was found. + if ( scalar @Attachments == 1 ) { + for my $Attachment (@Attachments) { + $Article{Attachment} = $Attachment; + } + } + else { + $Article{Attachment} = \@Attachments; + } + } + + $Article{ArticleNumber} .= ''; # stringify + push @Articles, \%Article; + } + + $Test->{ExpectedReturnRemoteData}->{Data}->{Ticket} = { + %{ $Test->{ExpectedReturnRemoteData}->{Data}->{Ticket} }, + Articles => \@Articles, + }; + + # Remove some fields before comparison since they might differ. + if ( $Test->{IncludeTicketData} ) { + for my $Key (qw(Age Changed UnlockTimeout)) { + delete $RequesterResult->{Data}->{Ticket}->{$Key}; + } + } + + # Expect to find just a few request attributes in the result + my @ExpectAttributes = qw( Body Subject ); + for my $ExpectedArticle ( @{ $Test->{RequestData}->{Article} } ) { + push @{ $Test->{ExpectedReturnRemoteData}->{Data}->{Ticket}->{Articles} }, { + map { $_ => $ExpectedArticle->{$_} } @ExpectAttributes + }; + } + my $ReducedRequesterResult = dclone($RequesterResult); + $ReducedRequesterResult->{Data}->{Ticket}->{Articles} = []; + for my $ReceivedArticle ( @{ $RequesterResult->{Data}->{Ticket}->{Articles} } ) { + push @{ $ReducedRequesterResult->{Data}->{Ticket}->{Articles} }, { + map { $_ => $ReceivedArticle->{$_} } @ExpectAttributes + }; + } + $Self->IsDeeply( + $ReducedRequesterResult, + $Test->{ExpectedReturnRemoteData}, + "$Test->{Name} - Requester success status (needs configured and running webserver)", + ); + + if ( $Test->{ExpectedReturnLocalData} ) { + $Self->IsDeeply( + $LocalResult, + $Test->{ExpectedReturnLocalData}, + "$Test->{Name} - Local result matched with expected local call result.", + ); + } + else { + $Self->IsDeeply( + $LocalResult, + $Test->{ExpectedReturnRemoteData}, + "$Test->{Name} - Local result matched with remote result.", + ); + } + + # Update web service config to exclude ticket data in responses. + if ( $Test->{IncludeTicketData} ) { + $WebserviceConfig->{Provider}->{Operation}->{TicketUpdate}->{IncludeTicketData} = 0; + my $WebserviceUpdate = $WebserviceObject->WebserviceUpdate( + ID => $WebserviceID, + Name => $WebserviceName, + Config => $WebserviceConfig, + ValidID => 1, + UserID => $Self->{UserID}, + ); + $Self->True( + $WebserviceUpdate, + 'WebserviceUpdate - Turned off IncludeTicketData' + ); + } + + _RunAlsoExpect( $Self, $Test, $LocalResult, $RequesterResult ); +} + +# cleanup + +# delete web-service +my $WebserviceDelete = $WebserviceObject->WebserviceDelete( + ID => $WebserviceID, + UserID => $Self->{UserID}, +); +$Self->True( + $WebserviceDelete, + "Deleted web service $WebserviceID", +); + +# delete tickets +for my $TicketID (@TicketIDs) { + my $TicketDelete = $TicketObject->TicketDelete( + TicketID => $TicketID, + UserID => $Self->{UserID}, + ); + + # sanity check + $Self->True( + $TicketDelete, + "TicketDelete() successful for Ticket ID $TicketID", + ); +} + +# delete dynamic fields +my $DeleteFieldList = $DynamicFieldObject->DynamicFieldList( + ResultType => 'HASH', + ObjectType => 'Ticket', +); + +DYNAMICFIELD: +for my $DynamicFieldID ( sort keys %{$DeleteFieldList} ) { + + next DYNAMICFIELD if !$DynamicFieldID; + next DYNAMICFIELD if !$DeleteFieldList->{$DynamicFieldID}; + + next DYNAMICFIELD if $DeleteFieldList->{$DynamicFieldID} !~ m{ ^Unittest }xms; + + my $DynamicFieldConfig = $DynamicFieldObject->DynamicFieldGet( + ID => $DynamicFieldID, + ); + my $ValuesDeleteSuccess = $BackendObject->AllValuesDelete( + DynamicFieldConfig => $DynamicFieldConfig, + UserID => $Self->{UserID}, + ); + + $DynamicFieldObject->DynamicFieldDelete( + ID => $DynamicFieldID, + UserID => 1, + ); +} + +# cleanup cache +$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(); + +# Fix up test data to expect the latest Article IDs according to the number to be created +# Returns the last results of $ArticleObject->ArticleList() if any +sub _ExpectLatestArticles { + my ( $Test, $TicketID, $LocalRemote ) = @_; + my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); + my @ArticleList; + + if ( $Test->{RequestData}->{Article} ) { + my $NumArticles = @{ $Test->{RequestData}->{Article} }; + + my @ArticleList = ( + $ArticleObject->ArticleList( + TicketID => $TicketID, + ) + )[ -$NumArticles .. -1 ]; + + my @ArticleIDs = map { $_->{ArticleID} } @ArticleList; + if ( $Test->{"ExpectedReturn${LocalRemote}Data"} ) { + $Test->{"ExpectedReturn${LocalRemote}Data"}->{Data} = { + %{ $Test->{"ExpectedReturn${LocalRemote}Data"}->{Data} }, + ArticleIDs => \@ArticleIDs, + }; + } + } + return @ArticleList; +} + +# Run extra tests specified as code if any +sub _RunAlsoExpect { + my ( $Self, $Test, $LocalResult, $RemoteResult ) = @_; + return if !exists $Test->{AlsoExpect}; + + my $Tests = $Test->{AlsoExpect}; + for my $Sub ( $Tests->{All}, $Tests->{Local} ) { + $Sub->( $Self, $LocalResult, 'LocalResult' ) if defined $Sub; + } + for my $Sub ( $Tests->{All}, $Tests->{Remote} ) { + $Sub->( $Self, $RemoteResult, 'RemoteResult' ) if defined $Sub; + } + return; +} + +1;