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
+
+
+
+ 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
+
+
+
+ Hier können Sie einen Avatar für das Mitglied auswählen.
+
+
+
+ Einstellungen
+
+
+
+ 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 @@
+
+
+
+
+
+ Standard-Avatar
+
+
+
+ Der Standard-Avatar wird bei Mitgliedern angezeigt, die keinen eigene Avatar gesetzt haben.
+ #
+
+
+ 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'); ?>
+
+
+
+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