TEE-06 Backend Callbacks und Subpaletten

Aus Contao Community Documentation

MsgError.png Unvollständiger Artikel: dieser Artikel ist noch nicht sauber bearbeitet.

Bitte erweitere ihn und entferne erst anschliessend diesen Hinweis.

Tagebuch einer Extension-Entwicklung

betrifft
TYPOlight Version ab TL 2.8
Extensions Extension Creator


Von Callbacks und Subpaletten

Nachdem auch das englische Sprachfile fertig ist, geht es nun an die letzte fehlende Funktionalität der Backend-Maske. Zunächst gibt es aber noch einige Detailkorrekturen.

Da MySQL keinen Boolean-Datentyp bietet, hatte ich die Felder, die nur true/false sein können, als int(1) angelegt, mit den möglichen Werten 0/1. Das klappt auch prinzipiell, die Stati der Checkboxen werden gespeichert und wieder aus der Datenbank ausgelesen, aber ein Problem zeigt sich, wenn man nach so einem Feld filtern will: Ich will nach dem aktiv-Feld filtern können. Häufig sind die die "aktiven" Turnierpaare von Interesse, die nicht mehr aktiven verstopfen aber die Liste.

Wählt man dieses Feld nun als Filter-Feld aus, werden in der DropDown-Liste als Filtermöglichkeiten aber nur "Ja" und nochmals "Ja" angezeigt. Das Filtern klappt damit auch, bei dem einen "Ja" werden nur die aktiven Paare angezeigt, beim anderen "Ja" die inaktiven. Aber das ist natürlich nicht so gewollt.

Durch Abschauen bei anderen Extensions bin ich darauf gekommen, die Checkbox-Felder durch char(1) statt int(1) abzubilden. Das klappt genau so gut, und auch das Filtern funktioniert mit "Ja" und "Nein". Alle int(1)-Felder wurden entsprechend in char(1) verändert. Nach dem Anpassen der database.sql muss natürlich das Install-Tool ausgeführt werden, um die Änderungen in der Datenbank durchzuführen.

Die database.sql sieht jetzt so aus:

CREATE TABLE `tl_gw_turnierpaare` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `sorting` int(10) unsigned NOT NULL default '0',
  `tstamp` int(10) unsigned NOT NULL default '0',
  `partnernachname` varchar(64) NOT NULL default '',
  `partnervorname` varchar(64) NULL default NULL,
  `partnerinnachname` varchar(64) NULL default NULL,
  `partnerinvorname` varchar(64) NULL default NULL,
  `startgruppe` varchar(32) NOT NULL default '',
  `startklasselatein` varchar(12) NULL default NULL,
  `startklassestandard` varchar(12) NULL default NULL,
  `aktiv` char(1) NOT NULL default '',
  `aktivseit` int(4) NULL default NULL,
  `aktivbis` int(4) NULL default NULL,
  `resetpassword` char(1) NULL default '',
  `password` varchar(64) NULL default NULL,
  `bild` varchar(255) NULL default NULL,
  `anschrift` text NULL,
  `zeigeanschrift` char(1) NOT NULL default '',
  `telefon` varchar(32) NULL default NULL,
  `zeigetelefon` char(1) NOT NULL default '',
  `fax` varchar(32) NULL default NULL,
  `zeigefax` char(1) NOT NULL default '',
  `mobil` varchar(32) NULL default NULL,
  `zeigemobil` char(1) NOT NULL default '',
  `email` varchar(128) NULL default NULL,
  `zeigeemail` char(1) NOT NULL default '',
  `homepage` varchar(128) NULL default NULL,
  `zeigehomepage` char(1) NOT NULL default '',
  `beschreibung` text NULL,
  PRIMARY KEY  (`id`),
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Wer aufgepasst hat, dem ist auch noch ein neues Feld aufgefallen:

  `resetpassword` char(1) NULL default '',

Das werde ich gleich für das Aktivieren einer Subpalette benötigen. Der Inhalt des Feldes in der Datenbank wird später nicht gebraucht, aber leider muss das Feld vorhanden sein, um es so nutzen zu können, wie ich es vorhabe.

Für das password-Feld habe ich ich vor, dass ein dort eingegebenes Passwort SHA1-gehasht in der Datenbank abgelegt wird, nicht im Klartext. Dabei wird ein eventuell schon vorhandenes Passwort natürlich überschrieben.

Um Fehleingaben zu verhindern, möchte ich eine Checkbox anzeigen, die defaultmäßig "aus" ist. Erst wenn die Checkbox aktiviert ist, soll per AJAX das Passwortfeld angezeigt werden.

Zunächst fügen wir die Definition für das Checkbox-Feld in die field-Sektion des DCA-Records ein:

PHP-Code:

        'resetpassword' => array 
        ( 
            'label'                   => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['resetpassword'], 
            'inputType'               => 'checkbox', 
            'default'                 => '', 
            'eval'                    => array('mandatory'=>false, 'isBoolean' => true, 'submitOnChange' => true), 
        ),

submitOnChange bewirkt, dass das Formular neu geladen wird, wenn das Feld angeklickt wird; nur dann wird das Passwortfeld nachgeladen.

Dafür benötigen wir eine sogenannte "Subpalette". Die Checkbox (bei mir "resetpassword") muss als "__selector__" angegeben werden im DCA-Record:

    // Palettes 
    'palettes' => array 
    ( 
        '__selector__'                => array('resetpassword'), 
...

In der default-Sektion sieht die Palettendefinition so aus:

                                  .'{aktiv_legend:hide},aktiv,aktivseit,aktivbis;{password_legend:hide},resetpassword;'

Hier steht also der Name der Subpalette. Der Name des Passwortfeldes steht hier nicht mehr. Das wird in der subpalettes-Sektion angegeben:

    // Subpalettes 
    'subpalettes' => array 
    ( 
        'resetpassword'               => 'password' 
    ),

Dies bedeutet, dass das Feld password in die Subpalette resetpassword eingeblendet wird, wenn resetpassword aktiviert ist. Wird es deaktiviert, verschwindet die Subpalette wieder.

Passwort Checkbox

Passwort neu eingeben

Die Texte hierzu wurden in den Sprachfiles entsprechend erweitert und angepasst.

Für beide Felder, resetpassword und password, benötige ich besondere Funktionalitäten. resetpassword soll bei Öffnen der Backendmaske IMMER deaktiviert, das Passwort-Feld also versteckt sein - egal was in der Datenbank für das Feld steht. Dafür setze ich zunächst

            'default'                 => '',

was dafür sorgt, dass beim Anlegen eines neuen Datensatzes die Checkbox deaktiviert ist. Und dann gibt es noch die Option load_callback, in der eine Funktion angegeben werden kann, die beim Laden des Feldes aufgerufen wird (Zu den Details von Callbacks gleich mehr).

Hier habe ich versucht, durch return ' '; immer den Defaultwert zurückzugeben. Leider klappt das nicht richtig, wenn der Datensatz mit "speichern" gespeichert wird, aber geöffnet bleibt. Obwohl die Checkbox dann deaktiviert dargestellt wird, bleibt das Passwort-Feld trotzdem angezeigt und wird nicht versteckt. Erst durch manuelles Aktivieren und erneutes Deaktivieren verschwindet das Passwortfeld wieder. Ich weiß nicht, ob das ein Bug oder gewollt ist, zumindest gefiel es mir nicht.

Ein weiterer Versuch scheiterte mit dem save_callback: also einer Funktion, die aufgerufen wird, bevor das Feld in die Datenbank gespeichert wird. Hier versuchte ich ebenfalls durch ein return ' '; zu erzwingen, dass immer ein deaktiviertes Feld gespeichert wird, und damit auch beim erneuten Anzeigen des Formulars deaktiviert bleibt.

Das funktioniert optisch auch sehr gut. Leider werden dann aber keine Eingaben im Passwort-Feld gespeichert.

Der Grund wird sehr wahrscheinlich sein, dass der save_callback auf dem resetpassword-Feld ausgeführt wird, bevor der Input im password-Feld verarbeitet wird. Mein Save-Callback deaktiviert die Checkbox, und damit auch die Subpalette, und das Feld in der Subpalette wird gar nicht mehr ausgewertet oder gespeichert. Auch die Reihenfolge der Felddefinitionen hat darauf keinen Einfluss, es kommt wohl auf die Reihenfolge in der Palettendefinition an, und die kann ich nicht umdrehen.

An der Stelle war tiefe Frustration angesagt, aber ich habe das Problem anders lösen können. Beim resetpassword-Feld werden jetzt keine Callbacks verwendet.

Dafür aber beim password-Feld, was jetzt in der Definition so aussieht:

        'password' => array 
        ( 
            'label'                   => &$GLOBALS['TL_LANG']['tl_gw_turnierpaare']['password'], 
            'inputType'               => 'text', 
            'eval'                    => array('mandatory'=>false, 'minlength' => 1, 'maxlength' => 64), 
            'load_callback'           => array(array('tl_gw_turnierpaare','password_load_callback')), 
            'save_callback'           => array(array('tl_gw_turnierpaare','password_save_callback')) 
        ),

Durch diese Definition wird beim Laden des Feldes die Funktion password_load_callback und beim Speichern password_save_callback aufgerufen, die sich in der Klasse tl_gw_turnierpaare befinden. Diese Klasse lege ich in der DCA-Definitions-Datei /modules/gw_turnierpaare/dca/tl_gw_turnierpaare.php an.

class tl_gw_turnierpaare extends Backend 
{ 
    /** 
     * Import the back end user object 
     */ 
    public function __construct() 
    { 
        parent::__construct(); 
        $this->import('BackendUser', 'User'); 
    } 
 
  public function password_load_callback() 
  { 
 ... 
 } 
 
  public function password_save_callback($var, $dc) 
  { 
... 
  } 
}

Das Gerüst habe ich mir bei anderen Extensions ab geschaut, es scheint zumindest klug zu sein, von Backend zu erben, und den Konstruktur zu überschreiben. Vielleicht ist es auch nicht nötig, ich habe es nicht probiert. Ob der load_callback Parameter übergeben bekommt, weiß ich nicht, aber ich benötige keinen Parameter.

Der save_callback erhält den Wert des Feldes, das gespeichert werden soll ($var), und den DataContainer ($dc), der zu dem Formular gehört. Meist (wie auch hier) ist es DC_Table, der DataContainer für Datenbanktabellen.

Mein password-Feld in der Datenbank wird den SHA1-Hash, also einen langen String unverständlicher hexadezimaler Zahlen enthalten. Es nützt nichts, wenn ich den im Backend im Passwort-Feld anzeige. Dort will der Admin Klartext-Passwörter eingeben. Im Load-Callback setze ich den Wert des password-Felds also auf einen leeren String, egal was in der Datenbank steht:

  public function password_load_callback() 
  { 
    // Passwort-Feld immer leer anzeigen (Damit der User SHA1-Hash nicht sieht) 
    return ''; 
  }

Damit wird der User schon mal nicht vom "Müll" aus der Datenbank belästigt. Umgekehrt müssen wir aber nicht das Klartext-Passwort, sondern den Hash in die Datenbank schreiben. Das macht der save_callback:

  public function password_save_callback($var, $dc) 
  { 
    // Kein neues PW angegeben: Feld nicht ändern 
    if(strlen($var) < 1) return ''; 
    // Aktuellen Datensatz aus DB holen 
    $row = $this->Database->prepare("SELECT * FROM tl_gw_turnierpaare WHERE id=?") 
                              ->execute($dc->id); 
    // PW in Passwort und Salt aufspalten 
    list($strPassword, $strSalt) = explode(':', $row->password); 
    // Falls kein Salt vorhanden, dann erzeugen 
        if (!strlen($strSalt)) 
        { 
            $strSalt = substr(md5(uniqid('', true)), 0, 23); 
        } 
    // SHA1-Hash aus Salt+neuem Passwort berechnen, Salt anhängen 
    $pwd = sha1($strSalt . $var) . ':' . $strSalt; 
    // Das resetpassword-Feld löschen 
    $this->Database->prepare("UPDATE tl_gw_turnierpaare SET resetpassword='' WHERE id=?") 
                              ->executeUncached($dc->id); 
    return $pwd; 
  }

Falls kein Passwort angegeben wurde, macht der save_callback garnichts, und gibt einen leeren String zurück.

Das Feld Id des DataContainers enthält die id des aktuellen Datensatzes in der Datenbank. Um den Hash berechnen zu können, benötige ich das "alte" Passwort in der Datenbank. Darum hole ich mir erstmal den gesamten Datensatz mit der ID ab.

Die Hash-Erzeugung habe ich mir beim File /system/libraries/User.php abgeschaut und funktioniert genauso wie in der Userverwaltung von TYPOlight: Der Hash wird zusammen mit einem "Salt" erzeugt, der zusammen mit dem Hash (durch Doppelpunkt getrennt) im password-Feld abgespeichert wird. Existiert noch kein Salt, wird er erzeugt. Existiert der Salt schon, wird er weiterverwendet (und dafür muss ich das alte Passwort aus der Datenbank auslesen - um an den evtl. schon vorhandenen Salt zu kommen). Die Hash-Erzeugung läuft dann ziemlich straight-forward ab.

Und fast ganz am Ende nochmal der Knackpunkt: Hier setze ich das resetpassword-Feld in der Datenbank auf .

Das (und leider nur das) sorgt in allen Fällen dafür, dass beim Öffnen von Datensätzen die Subpalette für das password-Feld geschlossen ist.

Abschließend gebe ich den berechneten Hash zurück. Er wird dann in die Datenbank eingetragen.

Wichtiger Hinweis noch: load_callback und save_callback müssen doppelt geschachtelte Arrays sein, weil es mehrere Callbacks geben kann, die nacheinander aufgerufen werden. Der Feldwert wird jeweils durch alle durchgeschleust. Falls man aber z.B. einen onSubmitCallback für das ganze Formular vorgeben möchte, ist das nur ein einfaches Array mit Klassennamen und Methodenname, weil es hier nur einen Callback gibt. Das ist so leider in der Referenz der möglichen Callbacks nicht dokumentiert, und hat mir kurz graue Haare beschert.

Damit ist das Backend-Modul für die Turnierpaar-Tabelle erstmal fertig.

Weiter wird es dann (endlich) mit dem Frontendmodul für die Turnierpaarliste gehen.

Ansichten
Meine Werkzeuge

Contao Community Documentation

Atari Teenage Riot ist eine Mischung aus singen, schreien und sich übergeben.

Leo Unglaub
Navigation
Verstehen
Verwenden
Entwickeln
Verschiedenes
Werkzeuge