Passwort generieren
Im Folgenden zeige ich euch meine Vorgehensweise, um im Backend von TYPO3 7 LTS einen “Passwort generieren” Button für das Passwort der Backendbenutzer zu erstellen. Der Button soll rechts vom Passwortfeld erscheinen. Mit jedem Klick soll ein AJAX-Call gestartet, der einerseits das Passwort setzt und andererseits das Passwort im Klartext in einem Bootstrap-Panel anzeigt.
Solche Aufgaben für das Frontend gibt es im Netz wie Sand am Meer, aber im Backend läuft so einiges anders. Mir war wichtig, diese Aufgabe möglichst Core kompatibel, also ohne Tricksen, Biegen und Brechen umzusetzen.
Einen Wizard einfügen
Zunächst müsst ihr für das Passwort der be_users
Tabelle einen weiteren Wizard hinzufügen. Erstellt in eurem SitePackage die Datei [sitePackage]/Configuration/TCA/Overrides/be_users.php
mit diesem Inhalt:
<?php
$GLOBALS['TCA']['be_users']['columns']['password']['config']['wizards'] = array(
'generatePassword' => array(
'type' => 'userFunc',
'userFunc' => \StefanFroemken\SitePackage\Hooks\GeneratePassword::class . '->render',
)
);
Mit dem TCA Typ userFunc
und der Angabe einer PHP-Klasse könnt ihr euch nun komplett selbst, um das Rendering eines Wizards kümmern. Erstellt nun die Datei [SitePackage]/Classes/Hooks/GeneratePassword.php
:
<?php
namespace JWeiland\MyExt\Hooks;
use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* @license www.gnu.org/licenses/gpl.html GNU General Public License, version 3 or later
*/
class GeneratePassword
{
/**
* Render a wizard to generate a password
*
* @param array $parentArray
* @param AbstractFormElement $formElement
*
* @return string The rendered Wizard
*/
public function render(array $parentArray, AbstractFormElement $formElement)
{
// render the structure of the panel to show the password after Ajax request
$parentArray['item'] .= sprintf(
'
%s
',
LocalizationUtility::translate('generatedPassword', 'myExt')
);
// render the button
/** @var IconFactory $iconFactory */
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
return sprintf(
'%s',
$parentArray['uid'],
$parentArray['itemName'],
$iconFactory->getIcon('actions-document-synchronize')
);
}
}
Was hier nicht zu sehen ist: TYPO3 ruft die render()
Methode mithilfe von GeneralUtility::callUserFunction()
auf. Das Besondere daran: Alle Parameter werden als Referenzen übergeben. Das ergibt gleich 2 verschiedene Anwendungsbereiche.
In dem Array $parentArray
gibt es unter dem Key item
das vollständige HTML des Passwortfeldes. In meinem Beispiel füge ich ein leeres Bootstrap Panel hinzu. Einzig eine Überschrift habe ich diesem verpasst. In den Panel-Body wird später per JavaScript das Passwort dynamisch eingebunden.
Den eigentlichen Wizard müsst ihr über den Returnwert realisieren. Ich habe mich hier für ein einfaches Icon entschieden, das ich mithilfe der neuen IconFactory erstelle. Hinzu kommt eine CSS-Klasse und die beiden Werte uid
und name
als data
Attribute auf die ihr später mit z.B. jQuery wieder zugreifen könnt. Gerade beim Editieren von mehreren Backendbenutzern (z.B. inline) ist es wichtig diese Daten zu haben, damit ihr nicht beim Klick auf den Button auf einmal die Passwörter aller geöffneten Benutzer ändert. Aber dazu später mehr.
Leert nach dem Speichern den Systemcache (roter Blitz) oder im Installtool Flush Caches
. Der Wizard sollte zumindest schon zu sehen sein.
Vorbereitungen für das RequireJS Modul
Dieser Part hat richtig Zeit gekostet. Es gibt diverse Hooks mit denen ich das JavaScript mithilfe des PageRenderers hätte einbinden können, jedoch gibt es ein Problem, das man nicht sofort bemerkt.
Es ist ein gewaltiger Unterschied, ob ihr einen Datensatz über das List
Module direkt bearbeitet, oder ihr den Datensatz als Teil eines anderen Datensatzes (TCA Typ: inline) bearbeitet. Das Problem bei Typ inline
ist, dass das komplette HTML über AJAX nachgeladen wird. Wenn ich das JavaScript global einfüge, dann sind dem JavaScript diese neuen Felder nicht bekannt. Binde ich das JavaScript direkt in den be_users
Datensatz ein, dann klappt das zwar für das direkte Bearbeiten, jedoch nicht, wenn der be_users
Datensatz über inline
nachgeladen wird. Das JavaScript ist zwar da, wird aber nicht ausgeführt.
Erst bei genauer Analyse des AJAX-Requests bin ich auf einen interessanten Wert im json
aufmerksam geworden: scriptCall
.
Im FormInlineAjaxController wird dieser Wert erstmalig erstellt und über mergeChildResultIntoJsonResult()
je nach Eingabefeld immer weiter aufgefüllt. Jedes Eingabefeld in TYPO3 hat grundsätzlich die Möglichkeit über den Array-Key requireJsModules
diesen scriptCall
zu befüllen. Problem an der Sache: Die neue Formengine von TYPO3 bietet keinen Hook an, um dieses Array zu befüllen. In Rücksprache mit dem Core gibt es nur eine Lösung: Wir müssen das Rendering des Passwortfeldes komplett überschreiben und dies als eigenen renderType
registrieren.
Fügt dazu in eurer ext_localconf.php
folgende Zeilen ein:
// Add our own form elements, because we need the requireJSmodule for our password generation
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1454580921] = array(
'nodeName' => 'myExtPassword',
'priority' => '70',
'class' => \JWeiland\MyExt\Form\Element\InputTextElement::class,
);
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1454581671] = array(
'nodeName' => 'myExtRsaPassword',
'priority' => '70',
'class' => \JWeiland\MyExt\Form\Element\RsaInputElement::class,
);
Mit diesen Zeilen registriert ihr 2 neue renderTypen. 2 deshalb, weil wir je nachdem, ob rsaauth
installiert ist oder nicht einen anderen renderTypen benötigen. Diese Unterscheidung realisieren wir wieder über unsere vorhin angelegte Datei [sitePackage]/Configuration/TCA/Overrides/be_users.php
:
<?php
if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('rsaauth')) {
$GLOBALS['TCA']['be_users']['columns']['password']['config']['renderType'] = 'myExtRsaPassword';
} else {
$GLOBALS['TCA']['be_users']['columns']['password']['config']['renderType'] = 'myExtPassword';
}
Hier seht ihr auch wieder die zuvor registrierten nodeNames, die nun als renderTypen eingesetzt werden. Durch diese Angabe brecht ihr aus dem Standardrendering von TYPO3 aus und leitet das Rendering in eure eigenen PHP-Klassen um.
Kopiert euch die beiden Dateien aus dem Core in ein Verzeichnis eurer Extension:
sysext/backend/Classes/Form/Element/InputTextElement.php
sysext/rsaauth/Classes/Form/Element/RsaInputElement.php
Ich habe bei mir den Pfad vom Original übernommen: [sitePackage]/Classes/Form/Element/
Passt nun die namespace
Zeilen an eure Extension an und erweitert mit extends
die Originalklassen. Hier ein Ausschnitt, wie die Datei aussehen könnte:
<?php
namespace JWeiland\MyExt\Form\Element;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of TCEform elements of the type "input type=text"
*/
class InputTextElement extends \TYPO3\CMS\Backend\Form\Element\InputTextElement
{
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render()
{
/** @var IconFactory $iconFactory */
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$languageService = $this->getLanguageService();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$resultArray['requireJsModules'] = array(
'TYPO3/CMS/MyExt/GeneratePassword'
);
$isDateField = false;
...
Es klingt bescheuert, aber ihr müsst die komplette render()
Methode aus der Originalklasse kopieren, nur um diese 3 Zeilen für das requireJsModules
einzufügen.
Ähnlich sieht es in der Datei für das RsaInputElement
aus. Hier wird bereits ein Modul geladen, das wir entsprechend erweitern müssen:
$resultArray['requireJsModules'] = array(
'TYPO3/CMS/Rsaauth/RsaEncryptionModule',
'TYPO3/CMS/MyExt/GeneratePassword'
);
Das JavaScript als RequireJS Modul
TYPO3 bringt von Haus aus RequireJS mit und sucht automatisch in dem Pfad [SitePackage]/Resources/Public/JavaScript
nach RequireJS Modulen. Die Registrierung eines RequireJS Moduls haben wir bereits im vorherigen Abschnitt durchgeführt. Legt nun eine js-Datei an. Meine heißt GeneratePassword.js
mit folgendem Inhalt:
/**
* Module: TYPO3/CMS/MyExt/GeneratePassword
*/
define("TYPO3/CMS/MyExt/GeneratePassword", ["jquery"], function($) {
$(function() {
// hide all panels
$("div.myExtGeneratedPassword").hide();
// start ajax call onclick
$("a.myExtGeneratePassword").on("click", function(event) {
event.preventDefault();
var itemName = $(this).data("itemname");
var itemUid = $(this).data("itemuid");
$.ajax({
url: TYPO3.settings.ajaxUrls['myExtGeneratePassword'],
dataType: 'text',
cache: false,
success: function(response) {
// set new password
$("[data-formengine-input-name='" + itemName + "']").val(response);
$("input[name='" + itemName + "']")
.siblings("div.myExtGeneratedPassword")
.show()
.find("div.panel-body")
.find("code")
.text(response);
TBE_EDITOR.fieldChanged('be_users', itemUid, 'password', itemName);
}
});
});
});
});
Sehr wichtig: Anders als bei Extbase-Extensions müssen eure RequireJS Module mit dem TYPO3\CMS
Vendornamen registriert werden. Der 2te Parameter ist ein Array von Modulen, das wir für unser Modul zwingend benötigen. In unserem Modul benötigen wir jquery
, das wir dann im dritten Parameter über $
in unserem Modul zur Verfügung stellen.
Im Weiteren greift ihr nun auf die data
Attribute unseres Wizards zu, um dann damit das exakte Passwortfeld ansprechen zu können und das Passwort zu verändern. Außerdem befüllt ihr noch den panel-body und zeigt es an.
Das Ajax-Script
Backend AJAX-Scripte sind zunächst in der ext_localconf.php
zu registrieren:
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerAjaxHandler(
'myExtGeneratePassword',
\JWeiland\MyExt\Ajax\GeneratePassword::class . '->generate',
true
);
Dank des eindeutigen Namens myExtGeneratePassword
erzeugt TYPO3 für euch völlig automatisch eine vollständige AJAX-Url inkl. HashWerten, die ihr wie oben gesehen mit TYPO3.settings.ajaxUrls
in eurem AJAX-Call verwenden könnt. Mit dem 2ten Parameter könnt ihr eine PHP-Klasse verknüpfen, die bei dem AJAX-Call aufgerufen werden soll. Meine GeneratePassword.php
hat folgenden Inhalt:
<?php
namespace JWeiland\MyExt\Ajax;
use JWeiland\MyExt\Configuration\ExtConf;
use TYPO3\CMS\Core\Http\AjaxRequestHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @license www.gnu.org/licenses/gpl.html GNU General Public License, version 3 or later
*/
class GeneratePassword {
/**
* Generate a password
*
* @param array $ajaxParameters
* @param AjaxRequestHandler $ajaxRequestHandler
* @return string
*/
public function generate(array $ajaxParameters, AjaxRequestHandler $ajaxRequestHandler)
{
/** @var ExtConf $extConf */
$extConf = GeneralUtility::makeInstance(ExtConf::class);
$ajaxRequestHandler->setContent(array(
$this->generateStrongPassword(
$extConf->getPasswordLength(),
$extConf->getAllowedPasswordChars()
)
));
}
/**
* Generate a strong password
* This method was inspired by: gist.github.com/tylerhall/521810
*
* @param int $length
* @param string $availableSets
*
* @return string
*/
public function generateStrongPassword($length = 12, $availableSets = 'luds')
{
$sets = array();
if (strpos($availableSets, 'l') !== false) {
$sets[] = 'abcdefghjkmnpqrstuvwxyz';
}
if (strpos($availableSets, 'u') !== false) {
$sets[] = 'ABCDEFGHJKMNPQRSTUVWXYZ';
}
if (strpos($availableSets, 'd') !== false) {
$sets[] = '23456789';
}
if (strpos($availableSets, 's') !== false) {
$sets[] = '!@#$%&*?';
}
$all = '';
$password = '';
foreach ($sets as $set)
{
$password .= $set[array_rand(str_split($set))];
$all .= $set;
}
$all = str_split($all);
for ($i = 0; $i < $length - count($sets); $i++) {
$password .= $all[array_rand($all)];
}
$password = str_shuffle($password);
return $password;
}
}
Das Passwort konfigurierbar machen
Legt im Rootverzeichnis eurer Extension eine Datei mit dem Namen ext_conf_template.txt
an. Mit dieser Datei könnt ihr eure Extension über den Extensionmanager konfigurierbar machen:
# cat=password; type=int+; label = LLL:EXT:myExt/Resources/Private/Language/ExtConf.xlf:passwordLength
passwordLength = 12
# cat=password; type=boolean; label = LLL:EXT:myExt/Resources/Private/Language/ExtConf.xlf:passwordUseUpperCase
passwordUseUpperCase = 1
# cat=password; type=boolean; label = LLL:EXT:myExt/Resources/Private/Language/ExtConf.xlf:passwordUseLowerCase
passwordUseLowerCase = 1
# cat=password; type=boolean; label = LLL:EXT:myExt/Resources/Private/Language/ExtConf.xlf:passwordUseDigits
passwordUseDigits = 1
# cat=password; type=boolean; label = LLL:EXT:myExt/Resources/Private/Language/ExtConf.xlf:passwordUseSpecialChars
passwordUseSpecialChars = 1
Die Übersetzung: ExtConf.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff">
<file t3:id="1454342620" source-language="en" datatype="plaintext" original="messages" date="2016-02-01T17:03:45Z" product-name="myExt">
<body>
<trans-unit id="passwordLength">
<source>Length of password</source>
</trans-unit>
<trans-unit id="passwordUseLowerCase">
<source>Use lowercase letters</source>
</trans-unit>
<trans-unit id="passwordUseUpperCase">
<source>Use uppercase letters</source>
</trans-unit>
<trans-unit id="passwordUseDigits">
<source>Use digits</source>
</trans-unit>
<trans-unit id="passwordUseSpecialChars">
<source>Use special chars</source>
</trans-unit>
</body>
</file>
</xliff>
Besserer Zugriff auf die Configuration mit ExtConf
<?php
namespace JWeiland\MyExt\Configuration;
use TYPO3\CMS\Core\SingletonInterface;
/**
* @license www.gnu.org/licenses/gpl.html GNU General Public License, version 3 or later
*/
class ExtConf implements SingletonInterface
{
/**
* passwordLength
*
* @var int
*/
protected $passwordLength = 12;
/**
* passwordUseLowerCase
*
* @var bool
*/
protected $passwordUseLowerCase = true;
/**
* passwordUseUpperCase
*
* @var bool
*/
protected $passwordUseUpperCase = true;
/**
* passwordUseDigits
*
* @var bool
*/
protected $passwordUseDigits = true;
/**
* passwordUseSpecialChars
*
* @var bool
*/
protected $passwordUseSpecialChars = true;
/**
* allowedPasswordChars
*
* @var string
*/
protected $allowedPasswordChars = 'luds';
/**
* constructor of this class
* This method reads the global configuration and calls the setter methods.
*/
public function __construct()
{
// get global configuration
$extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['my_ext']);
if (is_array($extConf)) {
// call setter method foreach configuration entry
foreach ($extConf as $key => $value) {
$methodName = 'set' . ucfirst($key);
if (method_exists($this, $methodName)) {
$this->$methodName($value);
}
}
}
}
/**
* Returns the passwordLength
*
* @return int $passwordLength
*/
public function getPasswordLength()
{
if (empty($this->passwordLength)) {
return 12;
} else {
return $this->passwordLength;
}
}
/**
* Sets the passwordLength
*
* @param int $passwordLength
* @return void
*/
public function setPasswordLength($passwordLength)
{
$this->passwordLength = (int)$passwordLength;
}
/**
* Returns the passwordUseLowerCase
*
* @return bool $passwordUseLowerCase
*/
public function getPasswordUseLowerCase()
{
return $this->passwordUseLowerCase;
}
/**
* Sets the passwordUseLowerCase
*
* @param bool $passwordUseLowerCase
* @return void
*/
public function setPasswordUseLowerCase($passwordUseLowerCase)
{
$this->passwordUseLowerCase = (bool)$passwordUseLowerCase;
}
/**
* Returns the passwordUseUpperCase
*
* @return bool $passwordUseUpperCase
*/
public function getPasswordUseUpperCase()
{
return $this->passwordUseUpperCase;
}
/**
* Sets the passwordUseUpperCase
*
* @param bool $passwordUseUpperCase
* @return void
*/
public function setPasswordUseUpperCase($passwordUseUpperCase)
{
$this->passwordUseUpperCase = (bool)$passwordUseUpperCase;
}
/**
* Returns the passwordUseDigits
*
* @return bool $passwordUseDigits
*/
public function getPasswordUseDigits()
{
return $this->passwordUseDigits;
}
/**
* Sets the passwordUseDigits
*
* @param bool $passwordUseDigits
* @return void
*/
public function setPasswordUseDigits($passwordUseDigits)
{
$this->passwordUseDigits = (bool)$passwordUseDigits;
}
/**
* Returns the passwordUseSpecialChars
*
* @return bool $passwordUseSpecialChars
*/
public function getPasswordUseSpecialChars()
{
return $this->passwordUseSpecialChars;
}
/**
* Sets the passwordUseSpecialChars
*
* @param bool $passwordUseSpecialChars
* @return void
*/
public function setPasswordUseSpecialChars($passwordUseSpecialChars)
{
$this->passwordUseSpecialChars = (bool)$passwordUseSpecialChars;
}
/**
* Returns the allowedPasswordChars
*
* @return string $allowedPasswordChars
*
* @throws \Exception
*/
public function getAllowedPasswordChars()
{
$allowedPasswordChars = [];
if ($this->getPasswordUseLowerCase()) {
$allowedPasswordChars[] = 'l';
}
if ($this->getPasswordUseUpperCase()) {
$allowedPasswordChars[] = 'u';
}
if ($this->getPasswordUseDigits()) {
$allowedPasswordChars[] = 'd';
}
if ($this->getPasswordUseSpecialChars()) {
$allowedPasswordChars[] = 's';
}
if (empty($allowedPasswordChars)) {
throw new \Exception('The allowed password chars are not configured in the extension configuration. Set some in the extension manager', 1454665870);
}
return implode('', $allowedPasswordChars);
}
}