Files
tyforum/script/user_avatar.pl

490 lines
18 KiB
Perl
Executable File

#!/usr/bin/perl
#------------------------------------------------------------------------------
# mwForum - Web-based discussion forum
# Copyright (c) 1999-2015 Markus Wichitill
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#------------------------------------------------------------------------------
use strict;
use warnings;
no warnings qw(uninitialized redefine);
# Imports
use TyfMain;
#------------------------------------------------------------------------------
# Init
my ( $m, $cfg, $lng, $user, $userId ) = TyfMain->new( $_[0] );
# Check if access should be denied
$cfg->{avatars} or $m->error('errNoAccess');
$userId or $m->error('errNoAccess');
# Load additional modules
eval { require Image::Info }
or $m->error("Image::Info module not available.")
if $cfg->{avatarUpload};
# Get CGI parameters
my $optUserId = $m->paramInt('uid');
my $remove = $m->paramBool('remove');
my $avatarUpload = $m->paramBool('avatarUpload');
my $gallerySelect = $m->paramBool('gallerySelect');
my $gravatarSelect = $m->paramBool('gravatarSelect');
my $galleryFile = $m->paramStr('galleryFile');
my $gravatarEmail = $m->paramStr('gravatarEmail');
my $submitted = $m->paramBool('subm');
# Select which user to edit
my $optUser = $optUserId && $user->{admin} ? $m->getUser($optUserId) : $user;
$optUser or $m->error('errUsrNotFnd');
$optUserId = $optUser->{id};
# Shortcuts
my $avaUrlPath = "$cfg->{attachUrlPath}/avatars";
my $avaFsPath = "$cfg->{attachFsPath}/avatars";
my $avatar = $optUser->{avatar};
# Process form
if ($submitted) {
# Check request source authentication
$m->checkSourceAuth() or $m->formError('errSrcAuth');
# Process upload form upload action
if ( $avatarUpload && $cfg->{avatarUpload} ) {
my ( $upload, undef, $fileSize ) = $m->getUpload('file');
my $validFileSize = $fileSize <= $cfg->{avatarMaxSize};
$validFileSize || $cfg->{avatarResize} or $m->formError('errAvaSizeEx');
# Check image info
my $info =
$TyfMain::MP
? Image::Info::image_info( $upload->fh() )
: Image::Info::image_info( \$m->{cgi}->param('file') );
$info && !$info->{error} or $m->formError('errAvaFmtUns');
my $imgW = int( $info->{width} );
my $imgH = int( $info->{height} );
!ref($imgW) && !ref($imgH) or $m->formError('errAvaFmtUns');
my $ext = lc( $info->{file_ext} );
$ext =~ /^(?:jpg|png|gif)\z/ or $m->formError('errAvaFmtUns');
my $avaW = $cfg->{avatarWidth};
my $avaH = $cfg->{avatarHeight};
my $validSize = $imgW == $avaW && $imgH == $avaH;
$validSize || $cfg->{avatarResize} or $m->formError('errAvaDimens');
my $animated = $info->{GIF_Loop} ? 1 : 0;
$animated ||= apng( $m, $upload ) if $ext eq 'png';
!$animated || $cfg->{avatarResize} or $m->formError('errAvaNoAnim');
# If there's no error, finish action
if ( !@{ $m->{formErrors} } ) {
# Delete old avatar
unlink "$avaFsPath/$avatar" if $avatar && $avatar !~ /[\/:]/;
# Write avatar file
my $rnd = sprintf( "%04u", int( rand(9999) ) );
my $oldFileName = "$optUserId-$rnd.$ext";
my $oldFile = "$avaFsPath/$oldFileName";
my $updateFileName = $oldFileName;
$m->createDirectories($avaFsPath);
$m->saveUpload( 'file', $upload, $oldFile );
# Resize image if enabled and necessary
if ( !$validFileSize || !$validSize || $animated ) {
# Load modules
my $module;
if ( !$cfg->{noGd} && eval { require GD } ) {
$module = 'GD';
}
elsif ( !$cfg->{noImager} && eval { require Imager } ) {
$module = 'Imager';
}
elsif ( !$cfg->{noGMagick} && eval { require Graphics::Magick }
)
{
$module = 'Graphics::Magick';
}
elsif ( !$cfg->{noIMagick} && eval { require Image::Magick } ) {
$module = 'Image::Magick';
}
else {
$m->error("GD, Imager or Magick modules not available.");
}
# Determine values
my $shrW = $avaW / $imgW;
my $shrH = $avaH / $imgH;
my $shrF = $m->min( $shrW, $shrH, 1 );
my $dstW = int( $imgW * $shrF + .5 );
my $dstH = int( $imgH * $shrF + .5 );
my $dstX = int( $m->max( $avaW - $dstW, 0 ) / 2 + .5 );
my $dstY = int( $m->max( $avaH - $dstH, 0 ) / 2 + .5 );
$rnd = sprintf( "%04u", int( rand(99999) ) );
my $newFileName = "$optUserId-$rnd.png";
my $newFile = "$avaFsPath/$newFileName";
# Resize image
if ( $module eq 'GD' ) {
GD::Image->trueColor(1);
my $oldImg = GD::Image->new($oldFile)
or unlink($oldFile), $m->error('errAvaFmtUns');
my $newImg = GD::Image->new( $avaW, $avaH, 1 )
or $m->error("Avatar creation failed.");
$newImg->alphaBlending(0);
$newImg->saveAlpha(1);
$newImg->fill( 0, 0,
$newImg->colorAllocateAlpha( 255, 255, 255, 127 ) );
$newImg->copyResampled(
$oldImg, $dstX, $dstY, 0, 0, $dstW,
$dstH, $imgW, $imgH
);
open my $fh, ">:raw", $newFile
or $m->error("Avatar opening failed. $!");
print $fh $newImg->png()
or $m->error("Avatar storing failed. $!");
close $fh;
}
elsif ( $module eq 'Imager' ) {
my $oldImg = Imager->new( file => $oldFile )
or unlink($oldFile), $m->error('errAvaFmtUns');
$oldImg = $oldImg->scale(
xpixels => $dstW,
ypixels => $dstH,
qtype => 'mixing',
type => 'nonprop'
)
or
$m->error( "Avatar scaling failed. " . Imager->errstr() );
my $newImg = Imager->new(
xsize => $avaW,
ysize => $avaH,
channels => 4
)
or $m->error(
"Avatar creation failed. " . Imager->errstr() );
$newImg->paste(
img => $oldImg,
left => $dstX,
top => $dstY
)
or $m->error(
"Avatar pasting failed. " . $newImg->errstr() );
$newImg->write( file => $newFile )
or $m->error(
"Avatar storing failed. " . $newImg->errstr() );
}
elsif ($module eq 'Graphics::Magick'
|| $module eq 'Image::Magick' )
{
my $oldImg = $module->new()
or $m->error("Image creation failed.");
my $err = $oldImg->Read( $oldFile . "[0]" )
and unlink($oldFile), $m->error('errAvaFmtUns');
$err = $oldImg->Scale( width => $dstW, height => $dstH )
and $m->error("Avatar scaling failed. $err");
my $newImg = $module->new( size => "${avaW}x${avaH}" )
or $m->error("Avatar creation failed.");
$err = $newImg->Read("xc:transparent")
and $m->error("Avatar filling failed. $err");
$err = $newImg->Composite(
image => $oldImg,
x => $dstX,
y => $dstY
) and $m->error("Avatar compositing failed. $err");
$err = $newImg->Write( filename => $newFile )
and $m->error("Avatar storing failed. $err");
}
unlink($oldFile);
$m->setMode( $newFile, 'file' );
$updateFileName = $newFileName;
}
# Update user
$m->dbDo( "
UPDATE users SET showAvatars = 1, avatar = ? WHERE id = ?",
$updateFileName, $optUserId );
# Log action and finish
$m->logAction( 1, 'user', 'avaupl', $userId, 0, 0, 0, $optUserId );
$m->redirect(
'user_avatar',
uid => $optUserId,
msg => 'AvaChange'
);
}
}
# Process gallery form select action
elsif ( $gallerySelect && $cfg->{avatarGallery} ) {
# If there's no error, finish action
if ( !@{ $m->{formErrors} } ) {
if ( $galleryFile && -f "$avaFsPath/gallery/$galleryFile" ) {
# Update user
$galleryFile = "gallery/$galleryFile";
$m->dbDo( "
UPDATE users SET showAvatars = 1, avatar = ? WHERE id = ?", $galleryFile,
$optUserId );
# Delete uploaded avatar
unlink "$avaFsPath/$avatar" if $avatar && $avatar !~ /[\/:]/;
}
# Log action and finish
$m->logAction( 1, 'user', 'avasel', $userId, 0, 0, 0, $optUserId );
$m->redirect(
'user_profile',
uid => $optUserId,
msg => 'AvaChange'
);
}
}
# Process gravatar form select action
elsif ( $gravatarSelect && $cfg->{avatarGravatar} ) {
# Check if this looks like an email address
$gravatarEmail =~
/^[A-Za-z_0-9.+-]+?\@(?:[A-Za-z_0-9-]+\.)+[A-Za-z]{2,}\z/
or $m->formError('errEmlInval');
# If there's no error, finish action
if ( !@{ $m->{formErrors} } ) {
# Update user
$gravatarEmail = lc($gravatarEmail);
$gravatarEmail = "gravatar:$gravatarEmail";
$m->dbDo( "
UPDATE users SET showAvatars = 1, avatar = ? WHERE id = ?", $gravatarEmail,
$optUserId );
# Delete uploaded avatar
unlink "$avaFsPath/$avatar" if $avatar && $avatar !~ /[\/:]/;
# Log action and finish
$m->logAction( 1, 'user', 'avasel', $userId, 0, 0, 0, $optUserId );
$m->redirect(
'user_profile',
uid => $optUserId,
msg => 'AvaChange'
);
}
}
# Process remove form actions
elsif ( $remove && $avatar ) {
# If there's no error, finish action
if ( !@{ $m->{formErrors} } ) {
# Update user
$m->dbDo( "
UPDATE users SET avatar = '' WHERE id = ?", $optUserId );
# Delete uploaded avatar
unlink "$avaFsPath/$avatar" if $avatar !~ /[\/:]/;
# Log action and finish
$m->logAction( 1, 'user', 'avadel', $userId, 0, 0, 0, $optUserId );
$m->redirect(
'user_avatar',
uid => $optUserId,
msg => 'AvaChange'
);
}
}
else { $m->error('errParamMiss') }
}
# Print form
if ( !$submitted || @{ $m->{formErrors} } ) {
# Print header
$m->printHeader();
# Print page bar
my @navLinks = (
{
url => $m->url( 'user_profile', uid => $optUserId ),
txt => 'comUp',
ico => 'up'
}
);
$m->printPageBar(
mainTitle => $lng->{avaTitle},
subTitle => $optUser->{userName},
navLinks => \@navLinks
);
# Print hints and form errors
$m->printFormErrors();
# Print avatar upload form
if ( $cfg->{avatarUpload} ) {
print
"<form action=\"user_avatar$m->{ext}\" method=\"POST\" enctype=\"multipart/form-data\">\n",
"<div class=\"frm\">\n",
"<div class=\"hcl\"><span class=\"htt\">$lng->{avaUplTtl}</span></div>\n",
"<div class=\"ccl\">\n";
if ( !$avatar || $avatar =~ /[\/:]/ ) {
my $label =
$cfg->{avatarResize}
? $m->formatStr( $lng->{avaUplImgRsz},
{ size => $m->formatSize( $cfg->{maxAttachLen} ) } )
: $m->formatStr(
$lng->{avaUplImgExc},
{
size => $m->formatSize( $cfg->{avatarMaxSize} ),
width => $cfg->{avatarWidth},
height => $cfg->{avatarHeight}
}
);
print
"<label class=\"lbw\">$label\n",
"<input type=\"file\" name=\"file\" accept=\"image/*\" autofocus></label>\n",
$m->submitButton( 'avaUplUplB', 'attach', 'avatarUpload' );
}
else {
print
"<div><img class=\"ava\" src=\"$avaUrlPath/$avatar\" alt=\"\"></div>\n",
$m->submitButton( 'avaUplDelB', 'delete', 'remove' );
}
print
"<input type=\"hidden\" name=\"uid\" value=\"$optUserId\">\n",
$m->stdFormFields(),
"</div>\n",
"</div>\n",
"</form>\n\n";
}
# Print avatar gallery form
if ( $cfg->{avatarGallery} ) {
# Count how often avatars are already used
my $used = $m->fetchAllArray( "
SELECT avatar, COUNT(*)
FROM users
WHERE avatar LIKE 'gallery/%'
GROUP BY avatar" );
my %used = map( ( $_->[0] => $_->[1] ), @$used );
print
"<form class=\"agl\" action=\"user_avatar$m->{ext}\" method=\"POST\">\n",
"<div class=\"frm\">\n",
"<div class=\"hcl\"><span class=\"htt\">$lng->{avaGalTtl}</span></div>\n",
"<div class=\"ccl\">\n",
"<fieldset>\n";
require File::Glob;
for my $file (
File::Glob::bsd_glob(
"$avaFsPath/gallery/*.{jpg,png,gif}",
File::Glob::GLOB_NOCASE() | File::Glob::GLOB_BRACE()
)
)
{
$file = $m->decFsPath($file);
my $chk = $file eq "$avaFsPath/$avatar" ? 'checked' : "";
$file =~ s!.*[\\/:]!!;
my ($name) = $file =~ /(.*)\.\w+\z/;
my $usedNum = $used{"gallery/$file"};
my $title = $usedNum ? "$name ($usedNum users)" : $name;
print
"<label><input type=\"radio\" name=\"galleryFile\" value=\"$file\" $chk>",
"<img class=\"ava\" src=\"$avaUrlPath/gallery/$file\" title=\"$title\" alt=\"$name\"></label>\n";
}
print
"</fieldset>\n",
$m->submitButton( 'avaGalSelB', 'avatar', 'gallerySelect' ),
$avatar =~ /\//
? $m->submitButton( 'avaGalDelB', 'remove', 'remove' )
: "",
"<input type=\"hidden\" name=\"uid\" value=\"$optUserId\">\n",
$m->stdFormFields(),
"</div>\n",
"</div>\n",
"</form>\n\n";
}
# Print gravatar form
if ( $cfg->{avatarGravatar} ) {
$gravatarEmail =
index( $avatar, "gravatar:" ) == 0 ? substr( $avatar, 9 ) : "";
print
"<form action=\"user_avatar$m->{ext}\" method=\"POST\">\n",
"<div class=\"frm\">\n",
"<div class=\"hcl\"><span class=\"htt\">$lng->{avaGrvTtl}</span></div>\n",
"<div class=\"ccl\">\n",
"<datalist id=\"email\"><option value=\"$optUser->{email}\"></datalist>\n",
"<label class=\"lbw\">$lng->{avaGrvEmail}\n",
"<input type=\"email\" class=\"hwi\" name=\"gravatarEmail\" list=\"email\"",
" value=\"$gravatarEmail\"></label>\n",
$m->submitButton( 'avaGrvSelB', 'avatar', 'gravatarSelect' ),
$avatar =~ /:/
? $m->submitButton( 'avaGrvDelB', 'remove', 'remove' )
: "",
"<input type=\"hidden\" name=\"uid\" value=\"$optUserId\">\n",
$m->stdFormFields(),
"</div>\n",
"</div>\n",
"</form>\n\n";
}
# Log action and finish
$m->logAction( 3, 'user', 'avatar', $userId, 0, 0, 0, $optUserId );
$m->printFooter();
}
$m->finish();
#------------------------------------------------------------------------------
# Check if PNG is APNG (based on PD code by Foone/WAHa)
sub apng {
my $m = shift();
my $upload = shift();
my ( $fh, $bytes, $buffer );
if ($TyfMain::MP) { $fh = $upload->fh() }
else { open $fh, '<', \$m->{cgi}->param('file') }
seek( $fh, 0, 0 );
$bytes = read( $fh, $buffer, 24 );
seek( $fh, 0, 0 );
return undef if $bytes != 24;
my ( $magic1, $magic2, $length, $ihdr, $width, $height ) =
unpack( "NNNNNN", $buffer );
return undef
if $magic1 != 0x89504e47 || $magic2 != 0x0d0a1a0a || $ihdr != 0x49484452;
seek( $fh, 8, 0 );
while (1) {
$bytes = read( $fh, $buffer, 8 );
last if $bytes != 8;
my ( $length, $type ) = unpack( 'NA4', $buffer );
last if $type eq 'IDAT';
last if $type eq 'IEND';
if ( $type eq 'acTL' ) {
seek( $fh, 0, 0 );
return 1;
}
last if seek( $fh, $length + 4, 1 ) == 0;
}
seek( $fh, 0, 0 );
return 0;
}