Files
2024-10-14 00:08:40 +02:00

536 lines
15 KiB
Perl

# --
# 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::System::Loader;
use strict;
use warnings;
use CSS::Minifier qw();
use JavaScript::Minifier qw();
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::Output::HTML::Layout',
'Kernel::System::Cache',
'Kernel::System::Log',
'Kernel::System::Main',
);
=head1 NAME
Kernel::System::Loader - CSS/JavaScript loader backend
=head1 DESCRIPTION
All valid functions.
=head1 PUBLIC INTERFACE
=head2 new()
create an object
my $LoaderObject = $Kernel::OM->Get('Kernel::System::Loader');
=cut
sub new {
my ( $Type, %Param ) = @_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
$Self->{CacheType} = 'Loader';
$Self->{CacheTTL} = 60 * 60 * 24 * 20;
return $Self;
}
=head2 MinifyFiles()
takes a list of files and returns a filename in the target directory
which holds the minified and concatenated content of the files.
Uses caching internally.
my $TargetFilename = $LoaderObject->MinifyFiles(
List => [ # optional, minify list of files
$Filename,
$Filename2,
],
Checksum => '...' # optional, pass a checksum for the minified file
Content => '...' # optional, pass direct (already minified) content instead of a file list
Type => 'CSS', # CSS | JavaScript
TargetDirectory => $TargetDirectory,
TargetFilenamePrefix => 'CommonCSS', # optional, prefix for the target filename
);
=cut
sub MinifyFiles {
my ( $Self, %Param ) = @_;
# check needed params
my $List = $Param{List};
my $Content = $Param{Content};
if ( !$Content && ( ref $List ne 'ARRAY' || !@{$List} ) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need List or Content!',
);
return;
}
my $TargetDirectory = $Param{TargetDirectory};
if ( !-e $TargetDirectory ) {
if ( !mkdir( $TargetDirectory, 0775 ) ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Can't create directory '$TargetDirectory': $!",
);
return;
}
}
if ( !$TargetDirectory || !-d $TargetDirectory ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need valid TargetDirectory, got '$TargetDirectory'!",
);
return;
}
my $TargetFilenamePrefix = $Param{TargetFilenamePrefix} ? "$Param{TargetFilenamePrefix}_" : '';
my %ValidTypeParams = (
CSS => 1,
JavaScript => 1,
);
if ( !$Param{Type} || !$ValidTypeParams{ $Param{Type} } ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need Type! Must be one of '" . join( ', ', keys %ValidTypeParams ) . "'."
);
return;
}
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
my $Filename;
if ( $Param{Checksum} ) {
$Filename = $TargetFilenamePrefix . $Param{Checksum};
}
else {
my $FileString;
if ( $Param{List} ) {
LOCATION:
for my $Location ( @{$List} ) {
if ( !-e $Location ) {
next LOCATION;
}
my $FileMTime = $MainObject->FileGetMTime(
Location => $Location
);
# For the caching, use both filename and mtime to make sure that
# caches are correctly regenerated on changes.
$FileString .= "$Location:$FileMTime:";
}
}
$Filename = $TargetFilenamePrefix . $MainObject->MD5sum(
String => \$FileString,
);
}
if ( $Param{Type} eq 'CSS' ) {
$Filename .= '.css';
}
elsif ( $Param{Type} eq 'JavaScript' ) {
$Filename .= '.js';
}
if ( !-r "$TargetDirectory/$Filename" ) {
# no cache available, so loop through all files, get minified version and concatenate
LOCATION: for my $Location ( @{$List} ) {
next LOCATION if ( !-r $Location );
# cut out the system specific parts for the comments (for easier testing)
# for now, only keep filename
my $Label = $Location;
$Label =~ s{^.*/}{}smx;
if ( $Param{Type} eq 'CSS' ) {
eval {
$Content .= $Self->GetMinifiedFile(
Location => $Location,
Type => $Param{Type},
);
};
if ($@) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Error during file minification: $@",
);
}
$Content .= "\n";
}
elsif ( $Param{Type} eq 'JavaScript' ) {
eval {
$Content .= $Self->GetMinifiedFile(
Location => $Location,
Type => $Param{Type},
);
};
if ($@) {
my $JSError = "Error during minification of file $Location: $@";
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => $JSError,
);
$JSError =~ s/'/\\'/gsmx;
$JSError =~ s/\r?\n/ /gsmx;
$Content .= "alert('$JSError');";
}
$Content .= "\n";
}
}
my $FileLocation = $MainObject->FileWrite(
Directory => $TargetDirectory,
Filename => $Filename,
Content => \$Content,
);
}
return $Filename;
}
=head2 GetMinifiedFile()
returns the minified contents of a given CSS or JavaScript file.
Uses caching internally.
my $MinifiedCSS = $LoaderObject->GetMinifiedFile(
Location => $Filename,
Type => 'CSS', # CSS | JavaScript
);
Warning: this function may cause a die() if there are errors in the file,
protect against that with eval().
=cut
sub GetMinifiedFile {
my ( $Self, %Param ) = @_;
# check needed params
my $Location = $Param{Location};
if ( !$Location ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Location!',
);
return;
}
my %ValidTypeParams = (
CSS => 1,
JavaScript => 1,
);
if ( !$Param{Type} || !$ValidTypeParams{ $Param{Type} } ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need Type! Must be one of '" . join( ', ', keys %ValidTypeParams ) . "'."
);
return;
}
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
my $FileMTime = $MainObject->FileGetMTime(
Location => $Location,
);
# For the caching, use both filename and mtime to make sure that
# caches are correctly regenerated on changes.
my $CacheKey = "$Location:$FileMTime";
# check if a cached version exists
my $CacheContent = $Kernel::OM->Get('Kernel::System::Cache')->Get(
Type => $Self->{CacheType},
Key => $CacheKey,
);
if ( ref $CacheContent eq 'SCALAR' ) {
return ${$CacheContent};
}
# no cache available, read and minify file
my $FileContents = $MainObject->FileRead(
Location => $Location,
# It would be more correct to use UTF8 mode, but then the JavaScript::Minifier
# will cause timeouts due to extreme slowness on some UT servers. Disable for now.
# Unicode in the files still works correctly.
#Mode => 'utf8',
);
if ( ref $FileContents ne 'SCALAR' ) {
return;
}
my $Result;
if ( $Param{Type} eq 'CSS' ) {
$Result = $Self->MinifyCSS( Code => $$FileContents );
}
elsif ( $Param{Type} eq 'JavaScript' ) {
$Result = $Self->MinifyJavaScript( Code => $$FileContents );
}
# and put it in the cache
$Kernel::OM->Get('Kernel::System::Cache')->Set(
Type => $Self->{CacheType},
TTL => $Self->{CacheTTL},
Key => $CacheKey,
Value => \$Result,
);
return $Result;
}
=head2 MinifyCSS()
returns a minified version of the given CSS Code
my $MinifiedCSS = $LoaderObject->MinifyCSS( Code => $CSS );
Warning: this function may cause a die() if there are errors in the file,
protect against that with eval().
=cut
sub MinifyCSS {
my ( $Self, %Param ) = @_;
# check needed params
if ( !$Param{Code} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Code Param!',
);
return;
}
my $Result = CSS::Minifier::minify( input => $Param{Code} );
# a few optimizations can be made for the minified CSS that CSS::Minifier doesn't yet do
# remove remaining linebreaks
$Result =~ s/\r?\n\s*//smxg;
# remove superfluous whitespace after commas in chained selectors
$Result =~ s/,\s*/,/smxg;
return $Result;
}
=head2 MinifyJavaScript()
returns a minified version of the given JavaScript Code.
my $MinifiedJS = $LoaderObject->MinifyJavaScript( Code => $JavaScript );
Warning: this function may cause a die() if there are errors in the file,
protect against that with eval().
This function internally uses the CPAN module JavaScript::Minifier.
As of version 1.05 of that module, there is an issue with regular expressions:
This will cause a die:
function test(s) { return /\d{1,2}/.test(s); }
A workaround is to enclose the regular expression in parentheses:
function test(s) { return (/\d{1,2}/).test(s); }
=cut
sub MinifyJavaScript {
my ( $Self, %Param ) = @_;
# check needed params
if ( !$Param{Code} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Code Param!',
);
return;
}
return JavaScript::Minifier::minify( input => $Param{Code} );
}
=head2 CacheGenerate()
generates the loader cache files for all frontend modules.
my %GeneratedFiles = $LoaderObject->CacheGenerate();
=cut
sub CacheGenerate {
my ( $Self, %Param ) = @_;
my @Result;
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
## nofilter(TidyAll::Plugin::OTRS::Perl::LayoutObject)
my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
my %AgentFrontends = %{ $ConfigObject->Get('Frontend::Module') // {} };
for my $FrontendModule ( sort { $a cmp $b } keys %AgentFrontends ) {
$LayoutObject->{Action} = $FrontendModule;
$LayoutObject->LoaderCreateAgentCSSCalls();
$LayoutObject->LoaderCreateAgentJSCalls();
push @Result, $FrontendModule;
}
my %CustomerFrontends = (
%{ $ConfigObject->Get('CustomerFrontend::Module') // {} },
%{ $ConfigObject->Get('PublicFrontend::Module') // {} },
);
for my $FrontendModule ( sort { $a cmp $b } keys %CustomerFrontends ) {
$LayoutObject->{Action} = $FrontendModule;
$LayoutObject->LoaderCreateCustomerCSSCalls();
$LayoutObject->LoaderCreateCustomerJSCalls();
push @Result, $FrontendModule;
}
# Now generate JavaScript translation content
for my $UserLanguage ( sort keys %{ $ConfigObject->Get('DefaultUsedLanguages') // {} } ) {
$Kernel::OM->ObjectsDiscard( Objects => ['Kernel::Language'] );
my $LocalLayoutObject = Kernel::Output::HTML::Layout->new(
Lang => $UserLanguage,
);
$LocalLayoutObject->LoaderCreateJavaScriptTranslationData();
}
# generate JS template cache
$LayoutObject->LoaderCreateJavaScriptTemplateData();
return @Result;
}
=head2 CacheDelete()
deletes all the loader cache files.
Returns a list of deleted files.
my @DeletedFiles = $LoaderObject->CacheDelete();
=cut
sub CacheDelete {
my ( $Self, %Param ) = @_;
my @Result;
my $Home = $Kernel::OM->Get('Kernel::Config')->Get('Home');
my $JSCacheFolder = "$Home/var/httpd/htdocs/js/js-cache";
my @SkinTypeDirectories = (
"$Home/var/httpd/htdocs/skins/Agent",
"$Home/var/httpd/htdocs/skins/Customer",
);
my @CacheFoldersList = ($JSCacheFolder);
my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
# Looking for all skin folders that may contain a cache folder
for my $Folder (@SkinTypeDirectories) {
my @List = $MainObject->DirectoryRead(
Directory => $Folder,
Filter => '*',
);
FOLDER:
for my $Folder (@List) {
next FOLDER if ( !-d $Folder );
my @CacheFolder = $MainObject->DirectoryRead(
Directory => $Folder,
Filter => 'css-cache',
);
if ( @CacheFolder && -d $CacheFolder[0] ) {
push @CacheFoldersList, $CacheFolder[0];
}
}
}
# now go through the cache folders and delete all .js and .css files
my @FileTypes = ( "*.js", "*.css" );
my $TotalCounter = 0;
FOLDERTODELETE:
for my $FolderToDelete (@CacheFoldersList) {
next FOLDERTODELETE if ( !-d $FolderToDelete );
my @FilesList = $MainObject->DirectoryRead(
Directory => $FolderToDelete,
Filter => \@FileTypes,
);
for my $File (@FilesList) {
if ( $MainObject->FileDelete( Location => $File ) ) {
push @Result, $File;
}
else {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Can't remove: $File"
);
}
}
}
# finally, also clean up the internal perl cache files
$Kernel::OM->Get('Kernel::System::Cache')->CleanUp(
Type => $Self->{CacheType},
);
return @Result;
}
1;
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L<https://otrs.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 L<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut