* @author Daniele Sciannimanica * @author Fabian Ekert * @copyright Oveleon */ namespace Oveleon\ContaoMemberExtensionBundle; use Contao\Config; use Contao\CoreBundle\Monolog\ContaoContext; use Contao\Dbafs; use Contao\File; use Contao\FilesModel; use Contao\FileUpload; use Contao\Frontend; use Contao\FrontendUser; use Contao\MemberModel; use Contao\StringUtil; use Contao\System; use Contao\Validator; use Psr\Log\LogLevel; /** * Class Member * * @property int $avatar UUID of the avatar */ class Member extends Frontend { const DEFAULT_PICTURE = 'bundles/contaomemberextension/avatar.png'; /** * MemberAvatar file name */ protected string $avatarName = 'memberAvatar'; /** * Create avatar for a member | Registration */ public function createAvatar(int $userId, array $arrData): void { $objMember = MemberModel::findById($userId); $this->processAvatar($objMember, $arrData); } /** * Update avatar of a member | Login */ public function updateAvatar(FrontendUser $objUser, array $arrData): void { $objMember = MemberModel::findById($objUser->id); $this->processAvatar($objMember, $arrData); } /** * Process avatar upload for a member */ protected function processAvatar(MemberModel $objMember, ?array $arrData): void { $objMember = MemberModel::findByPk($objMember->id); if ($objMember === null) { return; } // ToDo: remove $_SESSION when contao 4.13 support ends (Contao ^5.* is not possible with Contao 4.* support) $file = $_SESSION['FILES']['avatar']; $maxlength_kb = $this->getMaximumUploadSize(); $maxlength_kb_readable = $this->getReadableSize($maxlength_kb); // Sanitize the filename try { $file['name'] = StringUtil::sanitizeFileName($file['name']); } catch (\InvalidArgumentException $e) { // ToDo: add error message for invalid characters return; } // Invalid file name if (!Validator::isValidFileName($file['name'])) { // ToDo: add error message for invalid characters return; } // File was not uploaded if (!is_uploaded_file($file['tmp_name'])) { // ToDo: Add error messages /*if ($file['error'] == 1 || $file['error'] == 2) { // Add error message for maximum file size } elseif ($file['error'] == 3) { // Add error message for partial upload } elseif ($file['error'] > 0) { // Add error message for failed upload }*/ unset($_SESSION['FILES']['avatar']); return; } // File is too big if ($file['size'] > $maxlength_kb) { // ToDo: add error message for maximum file size unset($_SESSION['FILES']['avatar']); return; } $objFile = new File($file['name']); $uploadTypes = StringUtil::trimsplit(',', Config::get('validImageTypes')); // File type is not allowed if (!\in_array($objFile->extension, $uploadTypes)) { // ToDo: add error message for not allowed file type unset($_SESSION['FILES']['avatar']); return; } if ($arrImageSize = @getimagesize($file['tmp_name'])) { $intImageWidth = Config::get('imageWidth'); // Image exceeds maximum image width if ($intImageWidth > 0 && $arrImageSize[0] > $intImageWidth) { // ToDo: add error message for exceeding width unset($_SESSION['FILES']['avatar']); return; } $intImageHeight = Config::get('imageHeight'); // Image exceeds maximum image height if ($intImageHeight > 0 && $arrImageSize[1] > $intImageHeight) { // ToDo: add error message for exceeding height unset($_SESSION['FILES']['avatar']); return; } } // Upload valid file type with no width and height -> svg // Don't upload if no homedir is assigned // ToDo: Create homedir? if (!$objMember->assignDir || !$objMember->homeDir) { // ToDo: add error message for no homedir return; } $intUploadFolder = $objMember->homeDir; $objUploadFolder = FilesModel::findByUuid($intUploadFolder); // The upload folder could not be found if ($objUploadFolder === null) { throw new Exception("Invalid upload folder ID $intUploadFolder"); } $strUploadFolder = $objUploadFolder->path; // Store the file if the upload folder exists $projectDir = System::getContainer()->getParameter('kernel.project_dir'); if (!!$strUploadFolder & is_dir($projectDir . '/' . $strUploadFolder)) { // Delete existing avatar if it exists $this->deleteAvatar($objMember); $this->import('Files'); // Rename file $file['name'] = $this->avatarName . '.' . $objFile->extension; // Move the file to its destination $this->Files->move_uploaded_file($file['tmp_name'], $strUploadFolder . '/' . $file['name']); $this->Files->chmod($strUploadFolder . '/' . $file['name'], 0666 & ~umask()); $strUuid = null; $strFile = $strUploadFolder . '/' . $file['name']; // Generate the DB entries if (Dbafs::shouldBeSynchronized($strFile)) { $objModel = FilesModel::findByPath($strFile); if ($objModel === null) { $objModel = Dbafs::addResource($strFile); } $strUuid = StringUtil::binToUuid($objModel->uuid); // Update the hash of the target folder Dbafs::updateFolderHashes($strUploadFolder); // Update member avatar $objMember->avatar = $objModel->uuid; $objMember->save(); } // Add the session entry $_SESSION['FILES']['avatar'] = array ( 'name' => $file['name'], 'type' => $file['type'], 'tmp_name' => $projectDir . '/' . $strFile, 'error' => $file['error'], 'size' => $file['size'], 'uploaded' => true, 'uuid' => $strUuid ); // Add a log entry $logger = System::getContainer()->get('monolog.logger.contao'); $logger->log(LogLevel::INFO, 'File "' . $strUploadFolder . '/' . $file['name'] . '" has been uploaded', ['contao' => new ContaoContext(__METHOD__, TL_FILES)]); } unset($_SESSION['FILES']['avatar']); } /** * Return the maximum upload file size in bytes */ protected function getMaximumUploadSize() { if ($this->maxlength > 0) { return $this->maxlength; } return FileUpload::getMaxUploadSize(); } /** * Parses an avatar to the template */ public static function parseMemberAvatar(?MemberModel $objMember, &$objTemplate, ?string $imgSize): void { $objTemplate->addImage= true; $objTemplate->singleSRC = self::DEFAULT_PICTURE; $objTemplate->addFallbackImage = true; $projectDir = System::getContainer()->getParameter('kernel.project_dir'); // Check if member avatar exists if (null === $objMember || null === $objMember->avatar || null === ($objFile = FilesModel::findByUuid($objMember->avatar)) || !\is_file($projectDir.'/'. $objFile->path)) { $objFile = !!($uuidDefault = Config::get('defaultAvatar')) ? FilesModel::findByUuid($uuidDefault) : null; } // Check if config avatar exists if (null === $objFile || !\is_file($projectDir . '/' . $objFile->path)) { return; } $objTemplate->addFallbackImage = false; $imgSize = $imgSize ?? null; $figureBuilder = System::getContainer() ->get('contao.image.studio') ->createFigureBuilder() ->from($objFile->path) ->setSize($imgSize) ; if (null !== ($figure = $figureBuilder->buildIfResourceExists())) { $figure->applyLegacyTemplateData($objTemplate); } } /** * Gets the url for a member avatar */ public static function getMemberAvatarURL(?MemberModel $objMember): string { // ToDo: Merge logic with parseMemberAvatar $projectDir = System::getContainer()->getParameter('kernel.project_dir'); if (null === $objMember || null === $objMember->avatar || null === ($objFile = FilesModel::findByUuid($objMember->avatar)) || !\is_file($projectDir.'/'. $objFile->path)) { $objFile = !!($uuidDefault = Config::get('defaultAvatar')) ? FilesModel::findByUuid($uuidDefault) : null; } // Check if config avatar exists if (null === $objFile || !\is_file($projectDir . '/' . $objFile->path)) { return self::DEFAULT_PICTURE; } return $objFile->path; } /** * Deletes an avatar */ public static function deleteAvatar(MemberModel $objMember): void { if (!!$objMember->avatar) { $objFile = FilesModel::findByUuid($objMember->avatar) ?: ''; $projectDir = System::getContainer()->getParameter('kernel.project_dir'); // Only delete if file exists if (!!$objFile && file_exists($projectDir . '/' . $objFile->path)) { $file = new File($objFile->path); $file->delete(); } } } }