Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Service/User/Avatar.php
<?php

namespace XF\Service\User;

use
XF\Entity\User;

use function
count, in_array, intval;

class
Avatar extends \XF\Service\AbstractService
{
   
/**
     * @var User
     */
   
protected $user;

    protected
$logIp = true;
    protected
$logChange = true;

    protected
$fileName;

    protected
$width;

    protected
$height;

    protected
$cropX;
    protected
$cropY;

    protected
$type;

    protected
$error = null;

    protected
$allowedTypes = [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG];

    protected
$sizeMap;

    protected
$throwErrors = true;

    const
HIGH_DPI_THRESHOLD = 384;

    public function
__construct(\XF\App $app, User $user)
    {
       
parent::__construct($app);
       
$this->setUser($user);

       
$this->sizeMap = $this->app->container('avatarSizeMap');
    }

    protected function
setUser(User $user)
    {
        if (
$user->user_id)
        {
           
$this->user = $user;
        }
        else
        {
            throw new \
LogicException("User must be saved");
        }
    }

    public function
logIp($logIp)
    {
       
$this->logIp = $logIp;
    }

    public function
logChange($logChange)
    {
       
$this->logChange = $logChange;
    }

    public function
getError()
    {
        return
$this->error;
    }

    public function
silentRunning($runSilent)
    {
       
$this->throwErrors = !$runSilent;
    }

    public function
setImage($fileName)
    {
        if (!
$this->validateImageAsAvatar($fileName, $error))
        {
           
$this->error = $error;
           
$this->fileName = null;
            return
false;
        }

       
$this->fileName = $fileName;
        return
true;
    }

    public function
setImageFromUpload(\XF\Http\Upload $upload)
    {
       
$upload->requireImage();

        if (!
$upload->isValid($errors))
        {
           
$this->error = reset($errors);
            return
false;
        }

        return
$this->setImage($upload->getTempFile());
    }

    public function
setImageFromExisting()
    {
       
$path = $this->user->getAbstractedCustomAvatarPath('o');
        if (!
$this->app->fs()->has($path))
        {
            return
$this->throwException(new \InvalidArgumentException("User does not have an 'o' avatar ($path)"));
        }

       
$tempFile = \XF\Util\File::copyAbstractedPathToTempFile($path);
        return
$this->setImage($tempFile);
    }

   
/**
     * Sets the cropping values. These coordinates must be scaled to the medium size avatar (default 96px)!
     * Using null will automatically crop at the middle.
     *
     * @param int|null $x
     * @param int|null $y
     */
   
public function setCrop($x, $y)
    {
        if (
$x === null || $y === null)
        {
           
$this->cropX = $x;
           
$this->cropY = $y;
        }
        else
        {
           
$this->cropX = intval($x);
           
$this->cropY = intval($y);
        }
    }

    public function
getCrop()
    {
        return [
$this->cropX, $this->cropY];
    }

    public function
validateImageAsAvatar($fileName, &$error = null)
    {
       
$error = null;

        if (!
file_exists($fileName))
        {
            return
$this->throwException(new \InvalidArgumentException("Invalid file '$fileName' passed to avatar service"));
        }
        if (!
is_readable($fileName))
        {
            return
$this->throwException(new \InvalidArgumentException("'$fileName' passed to avatar service is not readable"));
        }

       
$imageInfo = filesize($fileName) ? @getimagesize($fileName) : false;
        if (!
$imageInfo)
        {
           
$error = \XF::phrase('provided_file_is_not_valid_image');
            return
false;
        }

       
$type = $imageInfo[2];
        if (!
in_array($type, $this->allowedTypes))
        {
           
$error = \XF::phrase('provided_file_is_not_valid_image');
            return
false;
        }

       
$width = $imageInfo[0];
       
$height = $imageInfo[1];

        if (!
$this->app->imageManager()->canResize($width, $height))
        {
           
$error = \XF::phrase('uploaded_image_is_too_big');
            return
false;
        }

       
$this->width = $width;
       
$this->height = $height;
       
$this->type = $type;

        return
true;
    }

    public function
updateAvatar()
    {
        if (!
$this->fileName)
        {
            return
$this->throwException(new \LogicException("No source file for avatar set"));
        }
        if (!
$this->user->exists())
        {
            return
$this->throwException(new \LogicException("User does not exist, cannot update avatar"));
        }

       
$imageManager = $this->app->imageManager();

       
$outputFiles = [];
       
$baseFile = $this->fileName;

       
$origSize = $this->sizeMap['o'];
       
$shortSide = min($this->width, $this->height);

        if (
$shortSide > $origSize)
        {
           
$image = $imageManager->imageFromFile($this->fileName);
            if (!
$image)
            {
                return
false;
            }

           
$image->resizeShortEdge($origSize);

           
$newTempFile = \XF\Util\File::getTempFile();
            if (
$newTempFile && $image->save($newTempFile, null, 95))
            {
               
$outputFiles['o'] = $newTempFile;
               
$baseFile = $newTempFile;
               
$width = $image->getWidth();
               
$height = $image->getHeight();
            }
            else
            {
                return
$this->throwException(new \RuntimeException("Failed to save image to temporary file; check internal_data/data permissions"));
            }

            unset(
$image);
        }
        else
        {
           
$outputFiles['o'] = $this->fileName;
           
$width = $this->width;
           
$height = $this->height;
        }

       
$crop = [
           
'm' => [0, 0]
        ];

        foreach (
$this->sizeMap AS $code => $size)
        {
            if (isset(
$outputFiles[$code]))
            {
                continue;
            }

           
$image = $imageManager->imageFromFile($baseFile);
            if (!
$image)
            {
                continue;
            }

           
$crop[$code] = $this->resizeAvatarImage($image, $size);

           
$newTempFile = \XF\Util\File::getTempFile();
            if (
$newTempFile && $image->save($newTempFile))
            {
               
$outputFiles[$code] = $newTempFile;
            }
            unset(
$image);
        }

        if (
count($outputFiles) != count($this->sizeMap))
        {
            return
$this->throwException(new \RuntimeException("Failed to save image to temporary file; image may be corrupt or check internal_data/data permissions"));
        }

        foreach (
$outputFiles AS $code => $file)
        {
           
$dataFile = $this->user->getAbstractedCustomAvatarPath($code);
            \
XF\Util\File::copyFileToAbstractedPath($file, $dataFile);
        }

       
$user = $this->user;
       
$user->bulkSet([
           
'avatar_date' => \XF::$time,
           
'avatar_width' => $width,
           
'avatar_height' => $height,
           
'avatar_highdpi' => ($width >= self::HIGH_DPI_THRESHOLD && $height >= self::HIGH_DPI_THRESHOLD),
           
'gravatar' => ''
       
]);

       
$profile = $user->getRelationOrDefault('Profile');
       
$profile->bulkSet([
           
'avatar_crop_x' => $crop['m'][0],
           
'avatar_crop_y' => $crop['m'][1]
        ]);
       
$user->addCascadedSave($profile);

        if (
$this->logChange == false)
        {
           
$user->getBehavior('XF:ChangeLoggable')->setOption('enabled', false);
        }

       
$user->save();

        if (
$this->logIp)
        {
           
$ip = ($this->logIp === true ? $this->app->request()->getIp() : $this->logIp);
           
$this->writeIpLog('update', $ip);
        }

        return
true;
    }

    protected function
resizeAvatarImage(\XF\Image\AbstractDriver $image, $size)
    {
       
$sizeMap = $this->sizeMap;

       
$cropX = $this->cropX;
       
$cropY = $this->cropY;
       
$cropScaleRef = $sizeMap['m'];
        if (
$cropX === null || $cropY === null)
        {
           
$cropScale = $sizeMap['o'] / $cropScaleRef;
           
$width = $image->getWidth();
           
$height = $image->getHeight();

           
$cropX = floor(
                ((
$width - $sizeMap['o']) / 2) / $cropScale
           
);
           
$cropY = floor(
                ((
$height - $sizeMap['o']) / 2) / $cropScale
           
);
        }

       
$cropX = max($cropX, 0);
       
$cropY = max($cropY, 0);

       
$image->resizeShortEdge($size, true);

       
$cropScale = $size / $cropScaleRef;
       
$thisCropX = floor($cropScale * $cropX);
       
$thisCropY = floor($cropScale * $cropY);

       
$widthOverage = $image->getWidth() - $size;
        if (
$widthOverage)
        {
           
$thisCropX = min($thisCropX, $widthOverage);
        }

       
$heightOverage = $image->getHeight() - $size;
        if (
$heightOverage)
        {
           
$thisCropY = min($thisCropY, $heightOverage);
        }

       
$image->crop($size, $size, $thisCropX, $thisCropY);

        return [
$thisCropX, $thisCropY];
    }

    public function
createOSizeAvatarFromL()
    {
       
$user = $this->user;

       
$l = $user->getAbstractedCustomAvatarPath('l');
       
$o = $user->getAbstractedCustomAvatarPath('o');
       
$fs = $this->app->fs();

        if (!
$fs->has($l) || $fs->has($o))
        {
            return
true;
        }

       
$fs->copy($l, $o);

       
$imageManager = $this->app->imageManager();
       
$lSize = $this->sizeMap['l'];

       
// temp file has original L image content
       
$tempFile = \XF\Util\File::copyAbstractedPathToTempFile($l);

       
$success = false;

        try
        {
           
$image = $imageManager->imageFromFile($tempFile);
            if (
$image)
            {
               
$this->resizeAvatarImage($image, $lSize);
               
$image->save($tempFile);
               
// temp file has new L image content
               
$success = true;
            }
            else
            {
               
// have to remove the avatar
               
$success = false;
            }
        }
        catch (\
Exception $e)
        {
            \
XF::logException($e, false, "Failed to update avatar for user {$user->user_id}: ");
        }

        if (
$success)
        {
            \
XF\Util\File::copyFileToAbstractedPath($tempFile, $l);
        }

        return
true;
    }

    public function
setGravatar($gravatar, $verify = true)
    {
       
$user = $this->user;

        if (
$gravatar !== '' && $verify)
        {
           
$validator = $this->app->validator('Gravatar');
            if (!
$validator->isValid($gravatar, $errorKey))
            {
               
$this->error = $validator->getPrintableErrorValue($errorKey);
                return
false;
            }
        }

       
$user->bulkSet([
           
'gravatar' => $gravatar
       
]);

        if (!
$user->preSave())
        {
           
$errors = $user->getErrors();
           
$this->error = reset($errors);
            return
false;
        }

       
$user->save();

        if (
$this->logIp)
        {
           
$ip = ($this->logIp === true ? $this->app->request()->getIp() : $this->logIp);
           
$this->writeIpLog('set_gravatar', $ip);
        }

        return
true;
    }

    public function
removeGravatar()
    {
       
$this->user->gravatar = '';
       
$this->user->saveIfChanged($changed);

        if (
$changed && $this->logIp)
        {
           
$ip = ($this->logIp === true ? $this->app->request()->getIp() : $this->logIp);
           
$this->writeIpLog('remove_gravatar', $ip);
        }

        return
true;
    }

    public function
deleteAvatar()
    {
       
$this->deleteAvatarFiles();

       
$user = $this->user;
       
$user->bulkSet([
           
'avatar_date' => 0,
           
'avatar_width' => 0,
           
'avatar_height' => 0,
           
'avatar_highdpi' => false,
           
'gravatar' => ''
       
]);

       
$profile = $user->getRelationOrDefault('Profile');
       
$profile->bulkSet([
           
'avatar_crop_x' => 0,
           
'avatar_crop_y' => 0
       
]);
       
$user->addCascadedSave($profile);

       
$user->save();

        if (
$this->logIp)
        {
           
$ip = ($this->logIp === true ? $this->app->request()->getIp() : $this->logIp);
           
$this->writeIpLog('delete', $ip);
        }

        return
true;
    }

    public function
deleteAvatarForUserDelete()
    {
       
$this->deleteAvatarFiles();

        return
true;
    }

    protected function
deleteAvatarFiles()
    {
        if (
$this->user->avatar_date)
        {
            foreach (
$this->sizeMap AS $code => $size)
            {
                \
XF\Util\File::deleteFromAbstractedPath($this->user->getAbstractedCustomAvatarPath($code));
            }
        }
    }

    protected function
writeIpLog($action, $ip)
    {
       
$user = $this->user;

       
/** @var \XF\Repository\Ip $ipRepo */
       
$ipRepo = $this->repository('XF:Ip');
       
$ipRepo->logIp(\XF::visitor()->user_id, $ip, 'user', $user->user_id, 'avatar_' . $action);
    }

   
/**
     * @param \Exception $error
     *
     * @return bool
     * @throws \Exception
     */
   
protected function throwException(\Exception $error)
    {
        if (
$this->throwErrors)
        {
            throw
$error;
        }
        else
        {
            return
false;
        }
    }
}