diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..25745c2 --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name":"oveleon/contao-member-extension-bundle", + "type":"contao-bundle", + "description":"Member feature extension for Contao.", + "keywords":["contao","member-extension-bundle"], + "homepage":"https://oveleon.de/", + "license":"MIT", + "authors":[ + { + "name":"Oveleon", + "homepage":"https://oveleon.de/", + "role":"Developer" + } + ], + "require":{ + "php":">=7.1", + "contao/core-bundle":"^4.4" + }, + "require-dev": { + "contao/manager-plugin": "^2.0" + }, + "conflict": { + "contao/core": "*", + "contao/core-bundle": "4.4.1", + "contao/manager-plugin": "<2.0 || >=3.0" + }, + "autoload":{ + "psr-4": { + "Oveleon\\ContaoMemberExtensionBundle\\": "src/" + }, + "classmap": [ + "src/Resources/contao/" + ], + "exclude-from-classmap": [ + "src/Resources/contao/config/", + "src/Resources/contao/dca/", + "src/Resources/contao/languages/", + "src/Resources/contao/templates/" + ] + }, + "extra":{ + "contao-manager-plugin": "Oveleon\\ContaoMemberExtensionBundle\\ContaoManager\\Plugin" + } +} \ No newline at end of file diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php new file mode 100644 index 0000000..b97bb33 --- /dev/null +++ b/src/ContaoManager/Plugin.php @@ -0,0 +1,33 @@ +setLoadAfter([ContaoCoreBundle::class]) + ->setReplace(['contao-member-extension-bundle']), + ]; + } +} diff --git a/src/ContaoMemberExtensionBundle.php b/src/ContaoMemberExtensionBundle.php new file mode 100644 index 0000000..1a1bde1 --- /dev/null +++ b/src/ContaoMemberExtensionBundle.php @@ -0,0 +1,17 @@ + + */ +class Member extends \Frontend +{ + /** + * Update avatar of member + * + * @param \FrontendUser $objUser + * @param array $arrData + */ + public function updateAvatar($objUser, $arrData) + { + $objMember = \MemberModel::findByPk($objUser->id); + + if ($objMember === null) + { + return; + } + + $file = $_SESSION['FILES']['avatar']; + $maxlength_kb = $this->getMaximumUploadSize(); + + // Sanitize the filename + try + { + $file['name'] = \StringUtil::sanitizeFileName($file['name']); + } + catch (\InvalidArgumentException $e) + { + // ToDo: Fehler: Dateiname beinhaltet unzulässige Zeichen + + return; + } + + // Invalid file name + if (!\Validator::isValidFileName($file['name'])) + { + // ToDo: Fehler: Dateiname beinhaltet unzulässige Zeichen + + return; + } + + // File was not uploaded + // ToDo + + // File is too big + if ($file['size'] > $maxlength_kb) + { + // ToDo: Fehler: Datei zu groß + 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: Fehler: Dateityp nicht erlaubt + 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: Fehler: Bild ist zu groß in der breite + unset($_SESSION['FILES']['avatar']); + + return; + } + + $intImageHeight = \Config::get('imageHeight'); + + // Image exceeds maximum image height + if ($intImageHeight > 0 && $arrImageSize[1] > $intImageHeight) + { + // ToDo: Fehler: Bild ist zu groß in der höhe + unset($_SESSION['FILES']['avatar']); + + return; + } + + $_SESSION['FILES']['avatar'] = $_SESSION['FILES']['avatar']; + + // Overwrite the upload folder with user's home directory + if ($objMember->assignDir && $objMember->homeDir) + { + $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 + if ($strUploadFolder != '' && is_dir(TL_ROOT . '/' . $strUploadFolder)) + { + $this->import('Files'); + + // Move the file to its destination + $this->Files->move_uploaded_file($file['tmp_name'], $strUploadFolder . '/' . $file['name']); + $this->Files->chmod($strUploadFolder . '/' . $file['name'], \Config::get('defaultFileChmod')); + + $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 (see #6986) + $_SESSION['FILES']['avatar'] = array + ( + 'name' => $file['name'], + 'type' => $file['type'], + 'tmp_name' => TL_ROOT . '/' . $strFile, + 'error' => $file['error'], + 'size' => $file['size'], + 'uploaded' => true, + 'uuid' => $strUuid + ); + + // Add a log entry + $this->log('File "' . $strUploadFolder . '/' . $file['name'] . '" has been uploaded', __METHOD__, TL_FILES); + } + } + + unset($_SESSION['FILES']['avatar']); + } + + /** + * Return the maximum upload file size in bytes + * + * @return string + */ + protected function getMaximumUploadSize() + { + // Get the upload_max_filesize from the php.ini + $upload_max_filesize = ini_get('upload_max_filesize'); + + // Convert the value to bytes + if (stripos($upload_max_filesize, 'K') !== false) + { + $upload_max_filesize = round($upload_max_filesize * 1024); + } + elseif (stripos($upload_max_filesize, 'M') !== false) + { + $upload_max_filesize = round($upload_max_filesize * 1024 * 1024); + } + elseif (stripos($upload_max_filesize, 'G') !== false) + { + $upload_max_filesize = round($upload_max_filesize * 1024 * 1024 * 1024); + } + + return min($upload_max_filesize, \Config::get('maxFileSize')); + } +} \ No newline at end of file diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php new file mode 100644 index 0000000..88aa8b9 --- /dev/null +++ b/src/Resources/contao/config/config.php @@ -0,0 +1,23 @@ + array('tl_member_settings'), + 'hideInNavigation' => true, +); + +// Front end modules +array_insert($GLOBALS['FE_MOD']['user'], -1, array +( + 'avatar' => '\\Oveleon\\ContaoMemberExtensionBundle\\ModuleAvatar' +)); + +// Register hooks +$GLOBALS['TL_HOOKS']['updatePersonalData'][] = array('\\Oveleon\\ContaoMemberExtensionBundle\\Member', 'updateAvatar'); diff --git a/src/Resources/contao/dca/tl_member.php b/src/Resources/contao/dca/tl_member.php new file mode 100644 index 0000000..4c06fd5 --- /dev/null +++ b/src/Resources/contao/dca/tl_member.php @@ -0,0 +1,35 @@ +addField(array('avatar'), 'personal_legend', Contao\CoreBundle\DataContainer\PaletteManipulator::POSITION_APPEND) + ->applyToPalette('default', 'tl_member') +; + +// Add global operations +array_insert($GLOBALS['TL_DCA']['tl_member']['list']['global_operations'], 0, array +( + 'settings' => array + ( + 'label' => &$GLOBALS['TL_LANG']['tl_member']['settings'], + 'href' => 'do=member_settings', + 'icon' => 'edit.svg', + 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"' + ) +)); + +// Add fields to tl_user +$GLOBALS['TL_DCA']['tl_member']['fields']['avatar'] = array +( + 'label' => &$GLOBALS['TL_LANG']['tl_member']['avatar'], + 'exclude' => true, + 'inputType' => 'fileTree', + 'eval' => array('feEditable'=>true, 'feViewable'=>true, 'feGroup'=>'personal', 'fieldType'=>'radio', 'filesOnly'=>true, 'isGallery'=>true, 'extensions'=>Config::get('validImageTypes'), 'tl_class'=>'clr'), + 'sql' => "binary(16) NULL" +); \ No newline at end of file diff --git a/src/Resources/contao/dca/tl_member_settings.php b/src/Resources/contao/dca/tl_member_settings.php new file mode 100644 index 0000000..8ab9597 --- /dev/null +++ b/src/Resources/contao/dca/tl_member_settings.php @@ -0,0 +1,35 @@ + array + ( + 'dataContainer' => 'File', + 'closed' => true + ), + + // Palettes + 'palettes' => array + ( + 'default' => '{avatar_legend},defaultAvatar;' + ), + + // Fields + 'fields' => array + ( + 'defaultAvatar' => array + ( + 'label' => &$GLOBALS['TL_LANG']['tl_member_settings']['defaultAvatar'], + 'inputType' => 'fileTree', + 'eval' => array('fieldType'=>'radio', 'filesOnly'=>true, 'isGallery'=>true, 'extensions'=>Config::get('validImageTypes'), 'tl_class'=>'clr') + ) + ) +); diff --git a/src/Resources/contao/dca/tl_module.php b/src/Resources/contao/dca/tl_module.php new file mode 100644 index 0000000..228ce4f --- /dev/null +++ b/src/Resources/contao/dca/tl_module.php @@ -0,0 +1,12 @@ + '{title_legend},name,headline,type;{source_legend},imgSize;{template_legend:hide},memberTpl;{protected_legend:hide},protected;{expert_legend:hide},guests,cssID' +)); diff --git a/src/Resources/contao/languages/de/modules.xlf b/src/Resources/contao/languages/de/modules.xlf new file mode 100644 index 0000000..f2f1ca3 --- /dev/null +++ b/src/Resources/contao/languages/de/modules.xlf @@ -0,0 +1,14 @@ + + + + + Avatar + Avatar + + + Displays the avatar of the member. + Zeigt den Avatar des Mitgliedes an. + + + + \ No newline at end of file diff --git a/src/Resources/contao/languages/de/tl_member.xlf b/src/Resources/contao/languages/de/tl_member.xlf new file mode 100644 index 0000000..9a84a2d --- /dev/null +++ b/src/Resources/contao/languages/de/tl_member.xlf @@ -0,0 +1,22 @@ + + + + + Avatar + Avatar + + + Here you can choose an avatar for the member. + Hier können Sie einen Avatar für das Mitglied auswählen. + + + Settings + Einstellungen + + + Member settings + Mitglieder-Einstellungen + + + + \ No newline at end of file diff --git a/src/Resources/contao/languages/de/tl_member_settings.xlf b/src/Resources/contao/languages/de/tl_member_settings.xlf new file mode 100644 index 0000000..6a88ea9 --- /dev/null +++ b/src/Resources/contao/languages/de/tl_member_settings.xlf @@ -0,0 +1,18 @@ + + + + + Default avatar + Standard-Avatar + + + The default avatar is displayed for members who have not set their own avatar. + Der Standard-Avatar wird bei Mitgliedern angezeigt, die keinen eigene Avatar gesetzt haben. + # + + Avatar + Avatar + + + + \ No newline at end of file diff --git a/src/Resources/contao/modules/ModuleAvatar.php b/src/Resources/contao/modules/ModuleAvatar.php new file mode 100644 index 0000000..b374910 --- /dev/null +++ b/src/Resources/contao/modules/ModuleAvatar.php @@ -0,0 +1,107 @@ + + */ +class ModuleAvatar extends \Module +{ + + /** + * Template + * @var string + */ + protected $strTemplate = 'member_avatar'; + + /** + * Return a wildcard in the back end + * + * @return string + */ + public function generate() + { + if (TL_MODE == 'BE') + { + /** @var BackendTemplate|object $objTemplate */ + $objTemplate = new \BackendTemplate('be_wildcard'); + + $objTemplate->wildcard = '### ' . Utf8::strtoupper($GLOBALS['TL_LANG']['FMD']['avatar'][0]) . ' ###'; + $objTemplate->title = $this->headline; + $objTemplate->id = $this->id; + $objTemplate->link = $this->name; + $objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id; + + return $objTemplate->parse(); + } + + // Return if user is not logged in + if (!FE_USER_LOGGED_IN) + { + return ''; + } + + if ($this->memberTpl != '') + { + $this->strTemplate = $this->memberTpl; + } + + return parent::generate(); + } + + /** + * Generate the module + */ + protected function compile() + { + $this->size = $this->imgSize; + + $this->import('FrontendUser', 'User'); + + if ($this->User->avatar == '' && \Config::get('defaultAvatar') == '') + { + return ''; + } + + if ($this->User->avatar == '') + { + $objFile = \FilesModel::findByUuid(\Config::get('defaultAvatar')); + + if ($objFile === null || !is_file(TL_ROOT . '/' . $objFile->path)) + { + return ''; + } + + $this->singleSRC = $objFile->path; + + $this->addImageToTemplate($this->Template, $this->arrData); + return; + } + + $objFile = \FilesModel::findByUuid($this->User->avatar); + + if ($objFile === null || !is_file(TL_ROOT . '/' . $objFile->path)) + { + $this->singleSRC = \FilesModel::findByUuid(\Config::get('defaultAvatar'))->path; + + $this->addImageToTemplate($this->Template, $this->arrData); + return; + } + + $this->singleSRC = $objFile->path; + + $this->addImageToTemplate($this->Template, $this->arrData, null, null, $objFile); + } +} diff --git a/src/Resources/contao/templates/member/member_avatar.html5 b/src/Resources/contao/templates/member/member_avatar.html5 new file mode 100644 index 0000000..3fbd2e6 --- /dev/null +++ b/src/Resources/contao/templates/member/member_avatar.html5 @@ -0,0 +1,9 @@ +extend('block_searchable'); ?> + +block('content'); ?> + +
+ insert('picture_default', $this->picture); ?> +
+ +endblock(); ?> diff --git a/src/Resources/public/avatar.png b/src/Resources/public/avatar.png new file mode 100644 index 0000000..b89587a Binary files /dev/null and b/src/Resources/public/avatar.png differ