# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# 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.
# --
package Kernel::Output::HTML::LinkObject::Ticket;
use strict;
use warnings;
use List::Util qw(first);
use Kernel::Output::HTML::Layout;
use Kernel::System::VariableCheck qw(:all);
use Kernel::Language qw(Translatable);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::Language',
'Kernel::Output::HTML::Layout',
'Kernel::System::CustomerCompany',
'Kernel::System::CustomerUser',
'Kernel::System::DynamicField',
'Kernel::System::DynamicField::Backend',
'Kernel::System::JSON',
'Kernel::System::Log',
'Kernel::System::Priority',
'Kernel::System::State',
'Kernel::System::Type',
'Kernel::System::User',
'Kernel::System::Web::Request',
);
=head1 NAME
Kernel::Output::HTML::LinkObject::Ticket - layout backend module
=head1 DESCRIPTION
All layout functions of link object (ticket).
=head2 new()
create an object
$BackendObject = Kernel::Output::HTML::LinkObject::Ticket->new(
UserLanguage => 'en',
UserID => 1,
);
=cut
sub new {
my ( $Type, %Param ) = @_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
# check needed objects
for my $Needed (qw(UserLanguage UserID)) {
$Self->{$Needed} = $Param{$Needed} || die "Got no $Needed!";
}
# We need our own LayoutObject instance to avoid block data collisions
# with the main page.
$Self->{LayoutObject} = Kernel::Output::HTML::Layout->new( %{$Self} );
# define needed variables
$Self->{ObjectData} = {
Object => 'Ticket',
Realname => 'Ticket',
ObjectName => 'SourceObjectID',
};
# get the dynamic fields for this screen
$Self->{DynamicField} = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
Valid => 0,
ObjectType => ['Ticket'],
);
return $Self;
}
=head2 TableCreateComplex()
return an array with the block data
Return
%BlockData = (
{
ObjectName => 'TicketID',
ObjectID => '14785',
Object => 'Ticket',
Blockname => 'Ticket',
Headline => [
{
Content => 'Number#',
Width => 130,
},
{
Content => 'Title',
},
{
Content => 'Created',
Width => 110,
},
],
ItemList => [
[
{
Type => 'Link',
Key => $TicketID,
Content => '123123123',
CssClass => 'StrikeThrough',
},
{
Type => 'Text',
Content => 'The title',
MaxLength => 50,
},
{
Type => 'TimeLong',
Content => '2008-01-01 12:12:00',
},
],
[
{
Type => 'Link',
Key => $TicketID,
Content => '434234',
},
{
Type => 'Text',
Content => 'The title of ticket 2',
MaxLength => 50,
},
{
Type => 'TimeLong',
Content => '2008-01-01 12:12:00',
},
],
],
},
);
@BlockData = $BackendObject->TableCreateComplex(
ObjectLinkListWithData => $ObjectLinkListRef,
);
=cut
sub TableCreateComplex {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ObjectLinkListWithData} || ref $Param{ObjectLinkListWithData} ne 'HASH' ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ObjectLinkListWithData!',
);
return;
}
# create needed objects
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
# convert the list
my %LinkList;
for my $LinkType ( sort keys %{ $Param{ObjectLinkListWithData} } ) {
# extract link type List
my $LinkTypeList = $Param{ObjectLinkListWithData}->{$LinkType};
for my $Direction ( sort keys %{$LinkTypeList} ) {
# extract direction list
my $DirectionList = $Param{ObjectLinkListWithData}->{$LinkType}->{$Direction};
for my $TicketID ( sort keys %{$DirectionList} ) {
$LinkList{$TicketID}->{Data} = $DirectionList->{$TicketID};
}
}
}
my $ComplexTableData = $ConfigObject->Get("LinkObject::ComplexTable");
my $DefaultColumns;
if (
$ComplexTableData
&& IsHashRefWithData($ComplexTableData)
&& $ComplexTableData->{Ticket}
&& IsHashRefWithData( $ComplexTableData->{Ticket} )
)
{
$DefaultColumns = $ComplexTableData->{"Ticket"}->{"DefaultColumns"};
}
my @TimeLongTypes = (
"Created",
"Changed",
"EscalationDestinationDate",
"FirstResponseTimeDestinationDate",
"UpdateTimeDestinationDate",
"SolutionTimeDestinationDate",
);
# define the block data
my $TicketHook = $ConfigObject->Get('Ticket::Hook');
my $TicketHookDivider = $ConfigObject->Get('Ticket::HookDivider');
my @Headline;
# Get needed objects.
my $UserObject = $Kernel::OM->Get('Kernel::System::User');
my $JSONObject = $Kernel::OM->Get('Kernel::System::JSON');
my $LanguageObject = $Kernel::OM->Get('Kernel::Language');
# load user preferences
my %Preferences = $UserObject->GetPreferences(
UserID => $Self->{UserID},
);
if ( !$DefaultColumns || !IsHashRefWithData($DefaultColumns) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Missing configuration for LinkObject::ComplexTable###Ticket!',
);
return;
}
# Get default column priority from SysConfig
# Each column in table (Title, State, Type,...) has defined Priority in SysConfig. System use this priority to sort columns, if user doesn't have own settings.
my %SortOrder;
if (
$ComplexTableData->{"Ticket"}->{"Priority"}
&& IsHashRefWithData( $ComplexTableData->{"Ticket"}->{"Priority"} )
)
{
%SortOrder = %{ $ComplexTableData->{"Ticket"}->{"Priority"} };
}
my %UserColumns = %{$DefaultColumns};
if ( $Preferences{'LinkObject::ComplexTable-Ticket'} ) {
my $ColumnsEnabled = $JSONObject->Decode(
Data => $Preferences{'LinkObject::ComplexTable-Ticket'},
);
if (
$ColumnsEnabled
&& IsHashRefWithData($ColumnsEnabled)
&& $ColumnsEnabled->{Order}
&& IsArrayRefWithData( $ColumnsEnabled->{Order} )
)
{
# Clear sort order
%SortOrder = ();
DEFAULTCOLUMN:
for my $DefaultColumn ( sort keys %UserColumns ) {
my $Index = 0;
for my $UserSetting ( @{ $ColumnsEnabled->{Order} } ) {
$Index++;
if ( $DefaultColumn eq $UserSetting ) {
$UserColumns{$DefaultColumn} = 2;
$SortOrder{$DefaultColumn} = $Index;
next DEFAULTCOLUMN;
}
}
# not found, means user chose to hide this item
if ( $UserColumns{$DefaultColumn} == 2 ) {
$UserColumns{$DefaultColumn} = 1;
}
if ( !$SortOrder{$DefaultColumn} ) {
$SortOrder{$DefaultColumn} = 0; # Set 0, it system will hide this item anyways
}
}
}
}
else {
# user has no own settings
for my $Column ( sort keys %UserColumns ) {
if ( !$SortOrder{$Column} ) {
$SortOrder{$Column} = 0; # Set 0, it system will hide this item anyways
}
}
}
# Define Headline columns
# Sort
my @AllColumns;
COLUMN:
for my $Column ( sort { $SortOrder{$a} <=> $SortOrder{$b} } keys %UserColumns ) {
my $ColumnTranslate = $Column;
if ( $Column eq 'EscalationTime' ) {
$ColumnTranslate = Translatable('Service Time');
}
elsif ( $Column eq 'EscalationResponseTime' ) {
$ColumnTranslate = Translatable('First Response Time');
}
elsif ( $Column eq 'EscalationSolutionTime' ) {
$ColumnTranslate = Translatable('Solution Time');
}
elsif ( $Column eq 'EscalationUpdateTime' ) {
$ColumnTranslate = Translatable('Update Time');
}
elsif ( $Column eq 'PendingTime' ) {
$ColumnTranslate = Translatable('Pending till');
}
elsif ( $Column eq 'CustomerCompanyName' ) {
$ColumnTranslate = Translatable('Customer Name');
}
elsif ( $Column eq 'CustomerID' ) {
$ColumnTranslate = Translatable('Customer ID');
}
elsif ( $Column eq 'CustomerName' ) {
$ColumnTranslate = Translatable('Customer User Name');
}
elsif ( $Column eq 'CustomerUserID' ) {
$ColumnTranslate = Translatable('Customer User ID');
}
elsif ( $Column =~ m{ \A DynamicField_ }xms ) {
my $DynamicFieldConfig;
DYNAMICFIELD:
for my $DFConfig ( @{ $Self->{DynamicField} } ) {
next DYNAMICFIELD if !IsHashRefWithData($DFConfig);
next DYNAMICFIELD if 'DynamicField_' . $DFConfig->{Name} ne $Column;
$DynamicFieldConfig = $DFConfig;
last DYNAMICFIELD;
}
next COLUMN if !IsHashRefWithData($DynamicFieldConfig);
$ColumnTranslate = $DynamicFieldConfig->{Label};
}
push @AllColumns, {
ColumnName => $Column,
ColumnTranslate => $ColumnTranslate,
};
# if enabled by default
if ( $UserColumns{$Column} == 2 ) {
my $ColumnName = '';
# Ticket fields
if ( $Column !~ m{\A DynamicField_}xms ) {
$ColumnName = $Column eq 'TicketNumber' ? $TicketHook : $ColumnTranslate;
}
# Dynamic fields (get label from the translated column).
else {
$ColumnName = $ColumnTranslate;
}
push @Headline, {
Content => $ColumnName,
};
}
}
# create the item list (table content)
my @ItemList;
for my $TicketID (
sort { $LinkList{$a}{Data}->{Age} <=> $LinkList{$b}{Data}->{Age} }
keys %LinkList
)
{
# extract ticket data
my $Ticket = $LinkList{$TicketID}{Data};
# set css
my $CssClass;
my @StatesToStrike = @{ $ConfigObject->Get('LinkObject::StrikeThroughLinkedTicketStateTypes') || [] };
if ( first { $Ticket->{StateType} eq $_ } @StatesToStrike ) {
$CssClass = 'StrikeThrough';
}
my @ItemColumns;
# Sort
COLUMN:
for my $Column ( sort { $SortOrder{$a} <=> $SortOrder{$b} } keys %UserColumns ) {
# if enabled by default
if ( $UserColumns{$Column} == 2 ) {
my %Hash;
if ( grep { $_ eq $Column } @TimeLongTypes ) {
$Hash{'Type'} = 'TimeLong';
}
else {
$Hash{'Type'} = 'Text';
}
if ( $Column eq 'Title' ) {
$Hash{MaxLength} = $Kernel::OM->Get('Kernel::Config')->Get('Ticket::SubjectSize') || 50;
}
# Ticket fields
if ( $Column !~ m{\A DynamicField_}xms ) {
if ( $Column eq 'TicketNumber' ) {
%Hash = (
Type => 'Link',
Key => $TicketID,
Content => $Ticket->{TicketNumber},
Link => $Self->{LayoutObject}->{Baselink}
. 'Action=AgentTicketZoom;TicketID='
. $TicketID,
Title => "$TicketHook$TicketHookDivider$Ticket->{TicketNumber}",
CssClass => $CssClass,
);
}
elsif ( $Column eq 'EscalationTime' ) {
$Hash{'Content'} = $Self->{LayoutObject}->CustomerAge(
Age => $Ticket->{'EscalationTime'},
Space => ' '
);
}
elsif ( $Column eq 'Age' ) {
$Hash{'Content'} = $Self->{LayoutObject}->CustomerAge(
Age => $Ticket->{Age},
Space => ' ',
);
}
elsif ( $Column eq 'EscalationSolutionTime' ) {
$Hash{'Content'} = $Self->{LayoutObject}->CustomerAge(
Age => $Ticket->{SolutionTime} || 0,
TimeShowAlwaysLong => 1,
Space => ' ',
);
}
elsif ( $Column eq 'EscalationResponseTime' ) {
$Hash{'Content'} = $Self->{LayoutObject}->CustomerAge(
Age => $Ticket->{FirstResponseTime} || 0,
TimeShowAlwaysLong => 1,
Space => ' ',
);
}
elsif ( $Column eq 'EscalationUpdateTime' ) {
$Hash{'Content'} = $Self->{LayoutObject}->CustomerAge(
Age => $Ticket->{UpdateTime} || 0,
TimeShowAlwaysLong => 1,
Space => ' ',
);
}
elsif ( $Column eq 'PendingTime' ) {
$Hash{'Content'} = $Self->{LayoutObject}->CustomerAge(
Age => $Ticket->{'UntilTime'},
Space => ' '
);
}
elsif ( $Column eq 'Owner' ) {
# get owner info
my %OwnerInfo = $Kernel::OM->Get('Kernel::System::User')->GetUserData(
UserID => $Ticket->{OwnerID},
);
$Hash{'Content'} = $OwnerInfo{'UserFullname'};
}
elsif ( $Column eq 'Responsible' ) {
# get responsible info
my %ResponsibleInfo = $Kernel::OM->Get('Kernel::System::User')->GetUserData(
UserID => $Ticket->{ResponsibleID},
);
$Hash{'Content'} = $ResponsibleInfo{'UserFullname'};
}
elsif ( $Column eq 'CustomerName' ) {
# get customer name
my $CustomerName;
if ( $Ticket->{CustomerUserID} ) {
$CustomerName = $Kernel::OM->Get('Kernel::System::CustomerUser')->CustomerName(
UserLogin => $Ticket->{CustomerUserID},
);
}
$Hash{'Content'} = $CustomerName;
}
elsif ( $Column eq 'CustomerCompanyName' ) {
my %CustomerCompany = $Kernel::OM->Get('Kernel::System::CustomerCompany')->CustomerCompanyGet(
CustomerID => $Ticket->{CustomerID},
);
$Hash{'Content'} = $CustomerCompany{CustomerCompanyName};
}
elsif ( $Column eq 'State' || $Column eq 'Priority' || $Column eq 'Lock' ) {
$Hash{'Content'} = $LanguageObject->Translate( $Ticket->{$Column} );
}
else {
$Hash{'Content'} = $Ticket->{$Column};
}
}
# Dynamic fields
else {
my $DynamicFieldConfig;
my $DFColumn = $Column;
$DFColumn =~ s{DynamicField_}{}g;
DYNAMICFIELD:
for my $DFConfig ( @{ $Self->{DynamicField} } ) {
next DYNAMICFIELD if !IsHashRefWithData($DFConfig);
next DYNAMICFIELD if $DFConfig->{Name} ne $DFColumn;
$DynamicFieldConfig = $DFConfig;
last DYNAMICFIELD;
}
next COLUMN if !IsHashRefWithData($DynamicFieldConfig);
# get field value
my $Value = $Kernel::OM->Get('Kernel::System::DynamicField::Backend')->ValueGet(
DynamicFieldConfig => $DynamicFieldConfig,
ObjectID => $TicketID,
);
my $ValueStrg = $Kernel::OM->Get('Kernel::System::DynamicField::Backend')->DisplayValueRender(
DynamicFieldConfig => $DynamicFieldConfig,
Value => $Value,
ValueMaxChars => 20,
LayoutObject => $Self->{LayoutObject},
);
$Hash{'Content'} = $ValueStrg->{Title};
}
push @ItemColumns, \%Hash;
}
}
push @ItemList, \@ItemColumns;
}
return if !@ItemList;
my %Block = (
Object => $Self->{ObjectData}->{Object},
Blockname => $Self->{ObjectData}->{Realname},
ObjectName => $Self->{ObjectData}->{ObjectName},
ObjectID => $Param{ObjectID},
Headline => \@Headline,
ItemList => \@ItemList,
AllColumns => \@AllColumns,
);
return ( \%Block );
}
=head2 TableCreateSimple()
return a hash with the link output data
Return
%LinkOutputData = (
Normal::Source => {
Ticket => [
{
Type => 'Link',
Content => 'T:55555',
Title => 'Ticket#555555: The ticket title',
CssClass => 'StrikeThrough',
},
{
Type => 'Link',
Content => 'T:22222',
Title => 'Ticket#22222: Title of ticket 22222',
},
],
},
ParentChild::Target => {
Ticket => [
{
Type => 'Link',
Content => 'T:77777',
Title => 'Ticket#77777: Ticket title',
},
],
},
);
%LinkOutputData = $BackendObject->TableCreateSimple(
ObjectLinkListWithData => $ObjectLinkListRef,
);
=cut
sub TableCreateSimple {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ObjectLinkListWithData} || ref $Param{ObjectLinkListWithData} ne 'HASH' ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ObjectLinkListWithData!'
);
return;
}
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
my $TicketHook = $ConfigObject->Get('Ticket::Hook');
my $TicketHookDivider = $ConfigObject->Get('Ticket::HookDivider');
my %LinkOutputData;
for my $LinkType ( sort keys %{ $Param{ObjectLinkListWithData} } ) {
# extract link type List
my $LinkTypeList = $Param{ObjectLinkListWithData}->{$LinkType};
for my $Direction ( sort keys %{$LinkTypeList} ) {
# extract direction list
my $DirectionList = $Param{ObjectLinkListWithData}->{$LinkType}->{$Direction};
my @ItemList;
for my $TicketID ( sort { $a <=> $b } keys %{$DirectionList} ) {
# extract ticket data
my $Ticket = $DirectionList->{$TicketID};
# set css
my $CssClass;
my @StatesToStrike = @{ $ConfigObject->Get('LinkObject::StrikeThroughLinkedTicketStateTypes') || [] };
if ( first { $Ticket->{StateType} eq $_ } @StatesToStrike ) {
$CssClass = 'StrikeThrough';
}
# define item data
my %Item = (
Type => 'Link',
Content => 'T:' . $Ticket->{TicketNumber},
Title => "$TicketHook$TicketHookDivider$Ticket->{TicketNumber}: $Ticket->{Title}",
Link => $Self->{LayoutObject}->{Baselink}
. 'Action=AgentTicketZoom;TicketID='
. $TicketID,
CssClass => $CssClass,
);
push @ItemList, \%Item;
}
# add item list to link output data
$LinkOutputData{ $LinkType . '::' . $Direction }->{Ticket} = \@ItemList;
}
}
return %LinkOutputData;
}
=head2 ContentStringCreate()
return a output string
my $String = $BackendObject->ContentStringCreate(
ContentData => $HashRef,
);
=cut
sub ContentStringCreate {
my ( $Self, %Param ) = @_;
# check needed stuff
if ( !$Param{ContentData} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need ContentData!'
);
return;
}
return;
}
=head2 SelectableObjectList()
return an array hash with select-able objects
Return
@SelectableObjectList = (
{
Key => 'Ticket',
Value => 'Ticket',
},
);
@SelectableObjectList = $BackendObject->SelectableObjectList(
Selected => $Identifier, # (optional)
);
=cut
sub SelectableObjectList {
my ( $Self, %Param ) = @_;
my $Selected;
if ( $Param{Selected} && $Param{Selected} eq $Self->{ObjectData}->{Object} ) {
$Selected = 1;
}
# object select list
my @ObjectSelectList = (
{
Key => $Self->{ObjectData}->{Object},
Value => $Self->{ObjectData}->{Realname},
Selected => $Selected,
},
);
return @ObjectSelectList;
}
=head2 SearchOptionList()
return an array hash with search options
Return
@SearchOptionList = (
{
Key => 'TicketNumber',
Name => 'Ticket#',
InputStrg => $FormString,
FormData => '1234',
},
{
Key => 'Title',
Name => 'Title',
InputStrg => $FormString,
FormData => 'BlaBla',
},
);
@SearchOptionList = $BackendObject->SearchOptionList(
SubObject => 'Bla', # (optional)
);
=cut
sub SearchOptionList {
my ( $Self, %Param ) = @_;
my $ParamHook = $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Hook') || 'Ticket#';
# search option list
my @SearchOptionList = (
{
Key => 'TicketNumber',
Name => $ParamHook,
Type => 'Text',
},
{
Key => 'TicketTitle',
Name => Translatable('Title'),
Type => 'Text',
},
{
Key => 'TicketFulltext',
Name => Translatable('Fulltext'),
Type => 'Text',
},
{
Key => 'StateIDs',
Name => Translatable('State'),
Type => 'List',
},
{
Key => 'PriorityIDs',
Name => Translatable('Priority'),
Type => 'List',
},
);
if ( $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Type') ) {
push @SearchOptionList,
{
Key => 'TypeIDs',
Name => Translatable('Type'),
Type => 'List',
};
}
if ( $Kernel::OM->Get('Kernel::Config')->Get('Ticket::ArchiveSystem') ) {
push @SearchOptionList,
{
Key => 'ArchiveID',
Name => Translatable('Archive search'),
Type => 'List',
};
}
# add formkey
for my $Row (@SearchOptionList) {
$Row->{FormKey} = 'SEARCH::' . $Row->{Key};
}
# add form data and input string
ROW:
for my $Row (@SearchOptionList) {
# prepare text input fields
if ( $Row->{Type} eq 'Text' ) {
# get form data
$Row->{FormData} = $Kernel::OM->Get('Kernel::System::Web::Request')->GetParam( Param => $Row->{FormKey} );
# parse the input text block
$Self->{LayoutObject}->Block(
Name => 'InputText',
Data => {
Key => $Row->{FormKey},
Value => $Row->{FormData} || '',
},
);
# add the input string
$Row->{InputStrg} = $Self->{LayoutObject}->Output(
TemplateFile => 'LinkObject',
);
next ROW;
}
# prepare list boxes
if ( $Row->{Type} eq 'List' ) {
# get form data
my @FormData = $Kernel::OM->Get('Kernel::System::Web::Request')->GetArray( Param => $Row->{FormKey} );
$Row->{FormData} = \@FormData;
my $Multiple = 1;
my %ListData;
if ( $Row->{Key} eq 'StateIDs' ) {
%ListData = $Kernel::OM->Get('Kernel::System::State')->StateList(
UserID => $Self->{UserID},
);
}
elsif ( $Row->{Key} eq 'PriorityIDs' ) {
%ListData = $Kernel::OM->Get('Kernel::System::Priority')->PriorityList(
UserID => $Self->{UserID},
);
}
elsif ( $Row->{Key} eq 'TypeIDs' ) {
%ListData = $Kernel::OM->Get('Kernel::System::Type')->TypeList(
UserID => $Self->{UserID},
);
}
elsif ( $Row->{Key} eq 'ArchiveID' ) {
%ListData = (
ArchivedTickets => Translatable('Archived tickets'),
NotArchivedTickets => Translatable('Unarchived tickets'),
AllTickets => Translatable('All tickets'),
);
if ( !scalar @{ $Row->{FormData} } ) {
$Row->{FormData} = ['NotArchivedTickets'];
}
$Multiple = 0;
}
# add the input string
$Row->{InputStrg} = $Self->{LayoutObject}->BuildSelection(
Data => \%ListData,
Name => $Row->{FormKey},
SelectedID => $Row->{FormData},
Size => 3,
Multiple => $Multiple,
Class => 'Modernize',
);
next ROW;
}
if ( $Row->{Type} eq 'Checkbox' ) {
# get form data
$Row->{FormData} = $Kernel::OM->Get('Kernel::System::Web::Request')->GetParam( Param => $Row->{FormKey} );
# parse the input text block
$Self->{LayoutObject}->Block(
Name => 'Checkbox',
Data => {
Name => $Row->{FormKey},
Title => $Row->{FormKey},
Content => $Row->{FormKey},
Checked => $Row->{FormData} || '',
},
);
# add the input string
$Row->{InputStrg} = $Self->{LayoutObject}->Output(
TemplateFile => 'LinkObject',
);
next ROW;
}
}
return @SearchOptionList;
}
1;
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L).
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 L.
=cut