Im Folgenden zeige ich euch meine Vorgehensweise, um im TYPO3-Backend einen Button zur Passwort-Generierung für Backend-Benutzer einzubinden. Der Button soll rechts vom Passwortfeld erscheinen. Bei jedem Klick wird per AJAX ein neues Passwort erzeugt, in das Eingabefeld eingesetzt und zusätzlich im Klartext in einem Panel unterhalb des Feldes angezeigt.
Lösungen für das Frontend gibt es im Netz wie Sand am Meer. Im Backend läuft die FormEngine jedoch nach ganz eigenen Regeln. Mir war wichtig, die Umsetzung möglichst sauber und Core-konform umzusetzen.
Einen Wizard einfügen
Zunächst müsst ihr für das Passwortfeld der Tabelle be_users einen zusätzlichen Wizard hinzufügen. Erstellt in eurer Extension bzw. eurem Sitepackage die Datei Configuration/TCA/Overrides/be_users.php mit folgendem Inhalt:
Mit dem TCA-Typ userFunc und der Angabe einer PHP-Klasse könnt ihr euch komplett selbst um das Rendering des Wizards kümmern. Erstellt nun die entsprechende Datei Classes/Hooks/GeneratePassword.php:
<?phpnamespaceVendor\MyExt\Hooks;
useTYPO3\CMS\Backend\Form\Element\AbstractFormElement;
useTYPO3\CMS\Core\Imaging\IconFactory;
useTYPO3\CMS\Core\Utility\GeneralUtility;
useTYPO3\CMS\Extbase\Utility\LocalizationUtility;
classGeneratePassword{
/**
* Render a wizard to generate a password
*
* @param array $parentArray
* @param AbstractFormElement $formElement
* @return string
*/publicfunctionrender(array $parentArray, AbstractFormElement $formElement):string {
// Structure of the panel to show the password after AJAX request
$parentArray['item'] .=sprintf(
'<div class="panel panel-default myExtGeneratedPassword" style="margin-top: 5px;">
<div class="panel-heading">%s</div>
<div class="panel-body"><code></code></div>
</div>',
LocalizationUtility::translate('generatedPassword', 'myExt')
);
// Render the button
/** @var IconFactory $iconFactory */ $iconFactory =GeneralUtility::makeInstance(IconFactory::class);
returnsprintf(
'<a href="#" class="btn btn-default myExtGeneratePassword" data-itemuid="%s" data-itemname="%s" title="Generate password">%s</a>',
$parentArray['uid'],
$parentArray['itemName'],
$iconFactory->getIcon('actions-document-synchronize')
);
}
}
Was man dem Code nicht direkt ansieht: TYPO3 ruft die Methode render() intern mittels GeneralUtility::callUserFunction() auf. Das Besondere dabei: Alle Parameter werden als Referenz übergeben. Das eröffnet zwei Möglichkeiten gleichzeitig:
Im Array $parentArray befindet sich unter dem Schlüssel item bereits das vollständige HTML des Passwortfeldes. In meinem Beispiel hänge ich dort ein leeres Bootstrap-Panel mit einer übersetzten Überschrift an. In den Panel-Body wird später per JavaScript das generierte Passwort im Klartext eingesetzt.
Den eigentlichen Wizard-Button liefert ihr über den Return-Wert der Funktion zurück. Ich erstelle hier einen Button mit dem Synchronisations-Icon aus der IconFactory. Wichtig sind die HTML5-Data-Attribute data-itemuid und data-itemname. Darüber kann JavaScript das Zielfeld im Formular eindeutig identifizieren. Das ist besonders dann essenziell, wenn mehrere Backend-Benutzer im gleichen Formular auf einmal bearbeitet werden (beispielsweise bei inline-Verknüpfungen). Ohne diese Zuordnung würdet ihr mit einem Klick versehentlich alle Passwortfelder auf der Seite überschreiben.
Nach dem Speichern solltet ihr den Cache im Backend leeren. Der Wizard-Button sollte danach am Passwortfeld zu sehen sein.
Vorbereitungen für das RequireJS-Modul
Dieser Teil hat bei der Entwicklung ordentlich Zeit gekostet. Es gibt zwar verschiedene Hooks, über die man JavaScript mithilfe des PageRenderer einbinden könnte, jedoch zeigt sich in der Praxis ein Detailproblem:
Es ist ein riesiger Unterschied, ob ein Datensatz direkt im Web-Modul Liste bearbeitet wird oder als verschachtelter Kind-Datensatz über den TCA-Typ inline geladen wird. Bei inline-Feldern wird das HTML nachträglich per AJAX nachgeladen. Wenn das JavaScript global eingebunden ist, kennt es die neu nachgeladenen DOM-Elemente zunächst nicht. Binde ich das JavaScript wiederum direkt im be_users-Datensatz ein, funktioniert es zwar beim direkten Bearbeiten, aber nicht beim Nachladen über inline. Das JavaScript wird im AJAX-Response zwar geliefert, aber vom Browser nicht ausgeführt.
Bei der Analyse der AJAX-Responses fiel mir im JSON-Objekt das Attribut scriptCall auf.
Im FormInlineAjaxController wird dieser Wert erstellt und über mergeChildResultIntoJsonResult() je nach Eingabefeld befüllt. Jedes Eingabefeld in TYPO3 kann über den Array-Schlüssel requireJsModules RequireJS-Module an diesen scriptCall übermitteln. Die FormEngine von TYPO3 bietet jedoch keinen Hook an, um dieses Array nachträglich zu erweitern. In Abstimmung mit dem Core bleibt hier der saubere Weg: Wir überschreiben das Rendering des Passwortfeldes über einen eigenen renderType.
Fügt dazu in eurer ext_localconf.php folgende Zeilen ein:
<?phpdefined('TYPO3') ||die('Access denied.');
// Add custom form elements to inject requireJSmodule for password generation
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1454580921] = [
'nodeName'=>'myExtPassword',
'priority'=>70,
'class'=>\Vendor\MyExt\Form\Element\InputTextElement::class,
];
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1454581671] = [
'nodeName'=>'myExtRsaPassword',
'priority'=>70,
'class'=>\Vendor\MyExt\Form\Element\RsaInputElement::class,
];
Damit werden zwei neue Node-Namen in der nodeRegistry registriert. Zwei deshalb, weil wir je nach Status der Extension rsaauth eine unterschiedliche Render-Klasse benötigen. Die Weiche setzen wir in der Datei Configuration/TCA/Overrides/be_users.php:
In der Extension liegt die Datei beispielsweise unter Classes/Form/Element/InputTextElement.php.
Passt den Namespace an eure Extension an und lasst eure Klasse von der jeweiligen Core-Klasse erben:
<?phpnamespaceVendor\MyExt\Form\Element;
useTYPO3\CMS\Core\Imaging\IconFactory;
useTYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Generation of TCEform elements of the type "input type=text"
*/classInputTextElementextends\TYPO3\CMS\Backend\Form\Element\InputTextElement{
/**
* Render single-line input form field with RequireJS module injection
*
* @return array As defined in initializeResultArray() of AbstractNode
*/publicfunctionrender():array {
$resultArray =parent::render();
$resultArray['requireJsModules'][] ='TYPO3/CMS/MyExt/GeneratePassword';
return $resultArray;
}
}
Dasselbe Prinzip wenden wir bei der Klasse RsaInputElement an:
<?phpnamespaceVendor\MyExt\Form\Element;
/**
* Generation of RSA password input elements with RequireJS module injection
*/classRsaInputElementextends\TYPO3\CMS\Rsaauth\Form\Element\RsaInputElement{
/**
* Render RSA password field with RequireJS module injection
*
* @return array
*/publicfunctionrender():array {
$resultArray =parent::render();
$resultArray['requireJsModules'][] ='TYPO3/CMS/MyExt/GeneratePassword';
return $resultArray;
}
}
Indem wir parent::render() aufrufen und lediglich das RequireJS-Modul an $resultArray['requireJsModules'] anhängen, vermeiden wir redundanten Code und halten die Lösung wartbar.
Das JavaScript als RequireJS-Modul
TYPO3 bringt RequireJS mit und sucht bei Modulen mit dem Prefix TYPO3/CMS/ in den Verzeichnissen der installierten Extensions. Legt die Datei Resources/Public/JavaScript/GeneratePassword.js in eurer Extension an:
Über die data-Attribute des Wizards steuern wir exakt das passende Passwortfeld an und aktualisieren dessen Wert. Zusätzlich fügen wir das generierte Passwort in den Body des Nachrichtenselements ein und signalisieren TYPO3 über TBE_EDITOR.fieldChanged(), dass sich der Feldinhalt geändert hat.
Das AJAX-Script im Backend
Backend-AJAX-Routen werden in der ext_localconf.php registriert:
Über die Kennung myExtGeneratePassword stellt TYPO3 in der JavaScript-Umgebung automatisch die passende URL unter TYPO3.settings.ajaxUrls['myExtGeneratePassword'] inklusive Sicherheits-Tokens bereit.
Erstellt nun die AJAX-Controller-Klasse Classes/Ajax/GeneratePassword.php: