Form Framework Double-Opt-In TYPO3 v8

EXT:Form Double-Opt-In

Die aktuelle TYPO3 Version 8 bringt das umstrittene neue „Forms“ Framework mit.
Während die Pflege und das Anlegen von Formularen und Formularfeldern nun auch für Redakteure über das Backend möglich ist, sind gewisse Features leider noch nicht Teil des Form-Frameworks.

Wir wollen uns heute mit der Erweiterung um einen Double-Opt-In kümmern.
Dazu benötigen wir eine eigene Extension, in diesem Zusammenhang möchten wir auch noch auf einige Best-Practices im Zusammenhang mit der Erweiterung des EXT: Form Frameworks verweisen.

Nachfolgend die Agenda des folgenden How-To’s:

  • Funktionsprinzip Double-Opt-In
  • Erweiterungen im Form Framework
  • Eigene Finisher
  • Hooks
  • Best Practices
  • Fazit

Funktionsprinzip:

Beim Double-Opt-In Anmeldeverfahren muss der Empfänger des Newsletters den Empfang des Newsletters bestätigen. Dies Dient dem Schutz vor ungewollten Emails.

Nehmen wir als Beispiel eine simple Newsletter-Anmeldung.

Füllt der User die Anmeldung aus, erhält er im Anschluss eine Email mit einem Bestätigungslink.
Erst wenn dieser Link aufgerufen wurde ist die Anmeldung abgeschlossen und der User wird in den Verteiler aufgenommen.

Erweiterungen im Form Framework:

Leider ist das Erweitern des neuen EXT:Form Frameworks nicht so trivial wie wir es uns anfangs vorstellten, dies ist hauptsächlich der Konfiguration über yaml Dateien zuzuschreiben. Hierzu empfiehlt es sich einen Blick in die Dokumentation von Form zu werfen:

https://docs.typo3.org/typo3cms/extensions/form/

Doch wir wollen in diesem Tutorial ganz von vorne beginnen.
Es fängt alles schon vor dem Erstellen des ersten Formulares an.

Um überhaupt ansatzweise erweiterte Konfigurationen vornehmen zu können müssen wir TYPO3 via TypoScript anweisen unsere CustomFormSetup.yaml zu laden, dies erfolgt über folgendes TypoScript Snippet:

plugin.tx_form {
    settings {
        yamlConfigurations {
            100 = EXT:YOUR_EXT/Configuration/Yaml/CustomFormSetup.yaml
        }
    }
}

module.tx_form {
    settings {
        yamlConfigurations {
            100 = EXT:YOUR_EXT/Configuration/Yaml/CustomFormSetup.yaml
        }
    }
}

Die dazugehörige CustomFormSetup.yaml erstellen wir ebenso, und füllen diese mit folgendem Inhalt:

TYPO3:
      CMS:
        Form:
          persistenceManager:
            allowedExtensionPaths:
              10: EXT:YOUR_EXT/Resources/Private/Forms/
            allowSaveToExtensionPaths: true
            allowDeleteFromExtensionPaths: true

          prototypes:
            contactform:
              __inheritances:
                10: 'TYPO3.CMS.Form.prototypes.standard'
              formElementsDefinition:
                Form:
                  renderingOptions:
                    templateRootPaths:
                      20: 'EXT:YOUR_EXT/Resources/Private/Fluid/Form/Frontend/Templates/'
                    partialRootPaths:
                      20: 'EXT:YOUR_EXT/Resources/Private/Fluid/Form/Frontend/Partials/'
                    layoutRootPaths:
                      20: 'EXT:YOUR_EXT/Resources/Private/Fluid/Form/Frontend/Layouts/'

 

Nachfolgend die wichtigsten Optionen erklärt:

  • allowedExtensionPaths: Der/Die Verzeichnis/e in denen die Forms abgelegt oder geladen werden können
  • allowSavetoExtensionPaths: Erlaubt das Speichern in diesen Pfad via den Form Editor

 

Des Weiteren können wir sogennante Prototypes definieren.

Den Prototypes können wir anschließend eigene Templates und in unserem Fall auch eigene Finisher zuweisen.

Haben wir die Dateien angelegt und das TypoScript eingebunden müsste uns nun beim Erstellen eines neuen Formulares die Option „Form Storage“ zur Verfügung stehen.

typo3-formhandler-double-opt-in_1

In unserem Fall soll uns ein Formular mit einem „E-Mail“ Feld reichen.

typo3-formhandler-double-opt-in_2

Nach dem Anlegen des Formular erscheint auch in unserem definiertem Pfad die double-Opt-In.form.yaml

renderingOptions:
  submitButtonLabel: Submit
type: Form
identifier: double-Opt-In
label: Double-Opt-In
prototypeName: standard
renderables:
  -
    renderingOptions:
      previousButtonLabel: 'Previous step'
      nextButtonLabel: 'Next step'
    type: Page
    identifier: page-1
    label: Step
    renderables:
      -
        defaultValue: ''
        type: Text
        identifier: text-1
        label: E-Mail
        properties:
          fluidAdditionalAttributes:
            required: required
        validators:
          -
            identifier: EmailAddress
          -
            identifier: NotEmpty

Als erstes wollen wir das Attribut prototypeName von ‚standard‘ zu unserem vorher definierten prototype ‚contactform‘ ändern.

Nun greift also für unser simples Kontaktformular die in der CustomFormSetup.yaml definierte Konfiguration.

Für das weitere Vorgehen greifen wir auf die double-Opt-In.form.yaml zu. Das Form Framework bringt eine Vielzahl nützlicher Finisher mit sich, doch nicht jeder Finisher lässt sich über das Backend konfigurieren. Dies gilt auch für den SaveToDatabase Finisher den wir für einen Double-Opt-In sogar zwei mal brauchen werden.

Davor erstellen wir aber noch ein verstecktes „uniquehash“ Feld welches wir später via Hook befüllen werden, sowie die Felder „confirmurl“ und „verifypid“. Wozu das sehen wir später. Dazu passen wir auch noch den identifier des E-Mail Feldes an:

renderables:
  -
    defaultValue: ''
    type: Text
    identifier: email
    label: E-Mail
    properties:
      fluidAdditionalAttributes:
        required: required
    validators:
      -
        identifier: EmailAddress
      -
        identifier: NotEmpty
  -
    identifier: uniquehash
    defaultValue: ''
    type: Hidden
  -
    identifier: confirmurl
    defaultValue: ''
    type: Hidden
  -
    identifier: verifypid
    defaultValue: ''
    type: Hidden

Die Konfiguration des Finishers ist leicht verständlich und geht schnell von der Hand.

Wir können nun den SaveToDatabase Finisher konfigurieren:

finishers:
  -
    identifier: SaveToDatabase
    options:
      table: tx_form_leads
      mode: insert
      databaseColumnMappings:
        enabled:
          value: '0'
      elements:
        email:
          mapOnDatabaseColumn: email
        uniquehash:
          mapOnDatabaseColumn: uniquehash

Natürlich muss die Tabelle tx_form_leads noch erstellt werden, dies geschieht bekanntlich über die ext_tables.sql

#
# Table structure for table 'tx_form_leads'
#
CREATE TABLE tx_form_leads (
  uid int(11) NOT NULL auto_increment,
  email varchar(255) DEFAULT '' NOT NULL,
  uniquehash varchar(255) DEFAULT '' NOT NULL,
 enabled tinyint(4) DEFAULT '0' NOT NULL,
  PRIMARY KEY (uid)
) ENGINE=InnoDB;

In der ext_localconf registrieren wir folgende Hooks:

# Register EXT:Form Hooks

// Set unique hash
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit']['1'] =
    \Bw\BaseBrowserwerk\Domain\Model\Renderable\setUniqueHash::class;

// Set the current BaseUrl
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit']['2'] =
    \Bw\BaseBrowserwerk\Domain\Model\Renderable\setBaseUrl::class;

Die Hooks schauen wie folgt aus:

class setUniqueHash
{
    /**
     * @param \TYPO3\CMS\Form\Domain\Runtime\FormRuntime $formRuntime
     * @param \TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface $renderable
     * @param mixed $elementValue submitted value of the element *before post processing*
     * @param array $requestArguments submitted raw request values
     * @return void
     */
    public function afterSubmit(\TYPO3\CMS\Form\Domain\Runtime\FormRuntime $formRuntime, \TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface $renderable, $elementValue, array $requestArguments = [])
    {
        $identifier = $renderable->getIdentifier();

        if($identifier == 'uniquehash')
        {
            $token = bin2hex(random_bytes(16));
            $elementValue = $token;
        }

        return $elementValue;
    }
}

class setBaseUrl
{
    /**
     * @param \TYPO3\CMS\Form\Domain\Runtime\FormRuntime $formRuntime
     * @param \TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface $renderable
     * @param mixed $elementValue submitted value of the element *before post processing*
     * @param array $requestArguments submitted raw request values
     * @return void
     */

    public function afterSubmit(\TYPO3\CMS\Form\Domain\Runtime\FormRuntime $formRuntime, \TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface $renderable, $elementValue, array $requestArguments = [])
    {
        $identifier = $renderable->getIdentifier();

        if($identifier == 'confirmurl')
        {
            $verifypid = $requestArguments['verifypid'];
            $url = $GLOBALS['TSFE']->cObj->typoLink_URL(
                array(
                    'parameter' => $verifypid,
                    'forceAbsoluteUrl' => true,
                )
            );
            $elementValue = $url;
        }

        return $elementValue;
    }
}

 

Schauen wir uns die Klasse setBaseUrl an wird uns klar warum wir die Felder confirmurl und verifypid brauchen.

Der Integrator/ Editor kann in „verifypid“ die ID der Seite eintragen auf der der User validiert wird.
Wir erstellen anschließend via typoLink eine URL, diese wird dem User in der Email zum verifizieren mitgegeben.

Füllen wir nun unser Form aus, erreichen wir schonmal dass wir einen Eintrag mit einer ID, E-Mail einem Hash und enabled = 0 in die Datenbank bekommen.

typo3-formhandler-double-opt-in_10

Selbstverständlich braucht der User noch eine Email mit der er seine Anmeldung bestätigen kann.

-
  identifier: EmailToReceiver
  options:
    subject: 'Bestätigung der Newsletter Anmeldung'
    recipientAddress: '{email}'
    recipientName: '{vorname} {nachname}'
    senderAddress: '{email}'
    senderName: 'Browserwerk Internetagentur'
    replyToAddress: ''
    carbonCopyAddress: ''
    blindCarbonCopyAddress: ''
    format: html
    attachUploads: 'false'
    templatePathAndFilename: 'EXT:base_browserwerk/Resources/Private/Forms/Email/newsletterAnmeldung.html'
    translation:
      language: ''

In der angegebenen HTML Datei können wir dann unser E-Mail Template erstellen. Über {form.formstate.formValues} sind wir in der Lage auf die Felder im Form zuzugreifen.

Ein einfaches Template könnte wie folgt aussehen:

{namespace formvh=TYPO3\CMS\Form\ViewHelpers}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <title></title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="format-detection" content="telephone=no">
</head>
<body>
<table width="600" cellpadding="0" cellspacing="0" border="0">
    <p>Sehr geehrter Interessent,</p>
    <p>vielen Dank, dass Sie sich über unsere Dienstleistungen per E-Mail informieren möchten.</p>
    <p>Klicken Sie bitte auf den folgenden Link, um die Registrierung abzuschließen:</p>
    <a href="{form.formstate.formValues.confirmurl}?uniquehash={form.formstate.formValues.uniquehash}">{form.formstate.formValues.confirmurl}{form.formstate.formValues.uniquehash}</a>

    <p>Viele Grüße aus Wiesbaden sendet Ihnen</p>
    <p>Ihr Browserwerk Team</p>
</table>
</body>
</html>

Damit das funktioniert müssen wir aber noch eine Seite anlegen auf welcher der Hash validiert wird.

typo3-formhandler-double-opt-in_13

In diesem Fall hat diese Seite die PID 6, also tragen wir diese in das Feld „verfiypid“ im Form ein:

typo3-formhandler-double-opt-in_14

Füllen wir das Formular aus erhalten wir auch schon folgende Email:

typo3-formhandler-double-opt-in_15

Wir haben nun also einen Datenbankeintrag, eine Email und einen Link welcher uns schon auf die richtige Seite führt.

Auf dieser Seite ist allerdings noch nichts zu sehen, und es gibt auch noch keine Art der Validierung.

Bekanntlich führen ja viele Wege nach Rom, und gerade für den nächsten Schritt gibt es eine Vielzahl von Möglichkeiten. Ich werde hier eine Möglichkeit präsentieren welche wie ich finde eine saubere und einfache Lösung darstellt den Hash zu validieren und den Eintrag in der Datenbank zu aktualisieren.

Wir werden ein weiteres Formular auf der Validierungsseite erstellen welches beim Abschicken in der Datenbank den übermittelten Hash sucht und den dazugehörigen Eintrag „enabled“ auf 1 setzt.

Um das Tutorial etwas kompakt zu halten gibt es hier meine komplette Form Definition des Bestätigungsformulars:

renderingOptions:
  submitButtonLabel: Submit
type: Form
identifier: newsletterBesttigung
label: 'Newsletter Bestätigung'
prototypeName: newsletter
finishers:
  -
    options:
      table: tx_form_leads
      mode: update
      whereClause:
        uniquehash: '{gethash}'
      databaseColumnMappings:
        enabled:
          value: '1'
    identifier: SaveToDatabase
renderables:
  -
    renderingOptions:
      previousButtonLabel: 'Previous step'
      nextButtonLabel: 'Next step'
    type: Page
    identifier: page-1
    label: Step
    renderables:
      -
        defaultValue: ''
        type: Text
        identifier: gethash
        label: ''

Wir brauchen aber einen weiteren Hook damit „gethash“ auch gefüllt wird.
Dieser wird wie gewohnt in der ext_localconf registriert:

// Get post parameter
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterBuildingFinished']['3'] =
    \Bw\BaseBrowserwerk\Domain\Model\Renderable\getPostParameter::class;

Der dazugehörige Hook:

class getPostParameter
{
    /**
     * @param \TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface $renderable
     * @return void
     */
    public function afterBuildingFinished(\TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface $renderable)
    {
        $identifier = $renderable->getIdentifier();
        if($identifier == 'gethash')
        {
            $GPvariable = \TYPO3\CMS\Core\Utility\GeneralUtility::_GP('uniquehash');
            $renderable->setDefaultValue($GPvariable);
            $pageRenderer = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Page\\PageRenderer');
            $pageRenderer->addJsFile(ExtensionManagementUtility::siteRelPath('base_browserwerk') . 'Resources/Public/Js/autosubmit.js);
        }
        return $elementValue;
    }
}

Der Hook sucht nach der Post Variable uniquehash, setzt diese in das Feld ‚gethash‘ und führt anschließend ein Autosubmit Script aus.

Die eigentliche Magie geschieht nun im Finisher des Validierungsformulars:

finishers:
  -
    options:
      table: tx_form_leads
      mode: update
      whereClause:
        uniquehash: '{gethash}'
      databaseColumnMappings:
        enabled:
          value: '1'
    identifier: SaveToDatabase

Als Abschluss empfehle ich noch den Redirect Finisher auf eine Dankeseite zu legen, so hat der User als Abschluss ein Feedback, dass alles geklappt hat.
Und ein Blick in die Datenbank bestätigt uns auch dass alles funktioniert hat:

typo3-formhandler-double-opt-in_20

Share This Post:

Browserwerk