Google Tag Manager mit eigenem Cookie-Banner. Tracking passiert erst bei nächstem Seitenaufruf, nicht direkt nach Einwilligung in Cookie-Banner.

Ich zeige im Schnelldurchlauf und relativ schematisch die Erkenntnisse des heutigen Tages: Probleme und Stolpersteine beim Einbinden des Google Tag Managers in Verbindung mit Google Analytics und einer eigenen Cookie-Consent Lösung.

Probleme mit eigenem Cookie-Consent-Banner und Google Tag Manager / Google Analytics

Möchte man einen eigenen Cookie-Banner umsetzen, der dem Google Tag Manager erlaubt, Analytics auf der Seite einzubinden, ergeben sich ein paar Probleme:

  1. Trotz korrekter Einbindung nach Anleitung wird die erste Seite, auf der auch die Einwilligung im Cookie-Banner passiert, nicht in Google Analytics getracked. Erst der nächste Seitenaufruf wird erfasst. Dadurch geht die Seite mit dem Cookie-Banner – sprich die erste Seite, die der User besucht – im Tracking verloren.
  2. Obwohl im Tag Manager für den Analytics Tag unter „Integrierte Einwilligungsprüfungen“ steht, dass als Default ad_storage und analytics_storage auf granted stehen müssen, bevor das Tracking beginnt, wird dies nicht beachtet. Auch beim Verwenden des default-dataLayers, der diese beiden Variablen auf „denied“ setzt, wird der pageView getrackt.

Zu Punkt 1 waren die Erkenntnisse:

  • Ein reines push in den dataLayer über gtag('consent', 'update', ...) mit granted führt nicht automatisch auch zu einem Einbinden von Google Analytics – und damit zu einem direkten PageView-Tracking!
  • Alles, was man am dataLayer für consent.update setzt, muss VOR der Einbindung des Tag-Managers-Script-Tags passieren, also z.B. unmittelbar nachdem man die Defaults über gtag('consent', 'default', ...) angibt. Der dataLayer wird komplett „gefüllt“, dann der Tag-Manager eingebunden, der den Container dann abholt und verarbeitet.
  • Wird im Nachhinein über gtag('consent', 'update', ...) die Einwilligung geändert passiert gar nichts.
  • Events hingegen, die man über dataLayer.push({'event': 'deinEventName'}); triggern kann, kommen beim Tag Manager an und erlauben es, auch NACH Laden des Tag-Managers Scripte und Tags in die Seite zu injecten / einzufügen.

Zu Punkt 2 waren die Erkenntnisse:

  • Die Doku scheint mir irreführend – oder es ist ein Bug. Im Tag-Manager klingt „Integrierte Einwilligungsprüfungen“ wie eine Voreinstellung, die „ad_storage“ und „analytics_storage“ mit „granted“ voraussetzt, damit der Google Analytics Tag eingebunden wird. Er wird aber auch mit consent.default und „denied“ direkt eingebunden.
  • Interessant daran: Es werden keine Cookies gesetzt – ein pageView-Tracking findet faktisch aber trotzdem statt, was aus DSGVO-Sicht kritisch ist.
  • Lösung ist es, „ad_storage“ und „analytics_storage“ explizit noch mal unter „Zusätzliche Einwilligung zur Auslösung des Tags erforderlich“ anzugeben.

Im Video werden die oben beschriebenen Punkte alle gezeigt.

Links:

Die Anleitung und Codes in der Google Tag Manager Dokumentation:
https://developers.google.com/tag-manager/consent

Das Tag Assistant Legacy Plugin für Chrome:
https://chrome.google.com/webstore/detail/tag-assistant-legacy-by-g/kejbdjndbnbjgmefkgdddjlbokphdefk

Und hier der finale Quelltext aus dem Video als Orientierung.
Bitte nur als Skizze / Funktionsprinzip verstehen 🙂

Denkt daran, das XXXXXXXXXXXXXXX mit Eurer Google-Tag-Manager-ID zu ersetzen.


<html> 
  
  <head>       
    <script> 
        
      window.dataLayer = window.dataLayer || [];  
  
      function gtag(){  
        dataLayer.push(arguments);  
      }  
      // Default ad_storage to 'denied'.  
      gtag('consent', 'default', {  
        'ad_storage': 'denied',  
        'analytics_storage': 'denied'  
      });  
  
      if (document.cookie.indexOf('dsgvo=1') >-1) {  
        consentWasGranted();  
      }  
  
      function consentWasGranted() {  
        gtag('consent', 'update', {  
          'ad_storage': 'granted',  
          'analytics_storage': 'granted'  
        });  
      }  
        
      function consentWasClicked() {  
        consentWasGranted();  
        dataLayer.push({'event': 'consentChange'});  
        document.cookie = 'dsgvo=1';  
      }  
  
    </script>   
      
    <!-- Google Tag Manager --> 
    <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':  
      new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],  
      j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=  
      'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);  
      })(window,document,'script','dataLayer','XXXXXXXXXXXXXXX');</script> 
      <!-- End Google Tag Manager --> 
    
  </head> 
    
    <body> 
    <button onclick="consentWasClicked()">Send EVENT</button> 
  </body> 
  
</html>

Typo3: Link zu einer Seite im Frontend aus Scheduler-Task / dem Backend-Kontext oder CLI-Job erzeugen

Jeder kommt früher oder später an diesen Punkt: Er ist in Typo3 in einem Context wie einem Scheduler Task oder CLI Job und möchte einen Link zu einer Seite im Frontend generieren, z.B. um zyklisch eine E-Mail zu generieren, die einen Link zum Frontend enthält.

Jeder Typo3-Entwickler geht hier zunächst den gewohnten Weg: Der UriBuilder. Den Code kopiert man sich aus einem Controller oder der Typo3 Doku – und er sieht ungefähr so aus:

$objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
$uriBuilder = $objectManager->get(\TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder::class);
$uri = $uriBuilder
  ->reset()
  ->setTargetPageUid(199)
  ->setArguments(['test'=>99])
  ->build();

Funktioniert wunderbar im Frontend-Context des Controllers – aber was passiert im Scheduler / Backend-Kontext?
Aus dem Link wird etwas in dieser Art:
/typo3/index.php?route=%2Fmodule%2Fsystem%2FtxschedulerM1&token=xxx&test=99
Für das Problem leider unbrauchbar. Und wieder eines der Themen, bei der man denkt: „Ist ganz einfach zu lösen“ – und sich dann wundert, wenn man nach einigen Stunden immer noch nicht die richtige Lösung finden konnte.

Leider ist das Web zu der Frage „Wie erzeugt man einen Link zum Frontend aus dem Backend-Kontext / aus dem Scheduler“ voll von Antworten, wie dieser hier bei StackOverflow: Allein beim Runterscrollen durch das Script denkt man eigentlich nur: Ist das Euer ernst? Ich will doch nur einen Link. Irgendwann baut man aus Ungeduld den Link dann vielleicht per Hand ($link = 'https://www.domain.de/?id=' . $pid) – aber richtig „gut“ fühlt sich das nicht an. Die URL ist hardgecoded, Parameter sind offen gelegt, der Slug / RealUrl fehlt. Nicht schön. Und erschwerend kommt hinzu: Der Code scheint sich von Typo3-Version zu Version zu ändern.

Wie es einfacher geht? Mit unserer Extension nnhelpers, die genau dort hilft, wo man denkt „Das muss doch irgendwie einfacher gehen“. nnhelpers ist eine Sammlung von Tools und Funktionen, die die Arbeit mit Typo3 und der Extension-Entwicklung erheblich vereinfacht. Alle Scripte sind als einzeilige „No-Brainer“ konzipiert. Und alle Methoden sind direkt im Backend in einem übersichtlichen „Spickzettel“ zusammengefasst.

So sieht die Lösung dann mit `nnhelpers` aus:

\nn\t3::Page()->getLink(199, ['test'=>99]);

Der gleiche Befehlt funktioniert in jedem Kontext – ob Frontend, Backend, CLI. Und er funktioniert versionsübergreifend für Typo3 6 aufwärts.
Er erzeugt einen Link zum Frontend aus dem Backend-Kontext oder Scheduler-Kontext oder CLI-Kontext mit absoluter URL zur Seite, bei der aber auch der Slug bzw. RealUrl-Pfad inkl. Parameter korrekt aufgelöst wird.

Richtig gesehen: `nnhelpers` braucht keine Instanz des `UriBuilder, der über den `ObjectManager` instanziiert werden muss, der über die `GeneralUtility` generiert werden muss. Der Einzeiler kann einfach – so wie er ist – an der Stelle eingefügt werden, wo er benötigt wird. Er hält damit den Code sauber und übersichtlich.

Hier geht es zur Extension im Repository von Typo3:
https://extensions.typo3.org/extension/nnhelpers

Die Dokumentation dazu gibt es hier:
Auf Deutsch: https://labor.99grad.de/typo3-docs/nnhelpers/de/
Und English: https://labor.99grad.de/typo3-docs/nnhelpers/en/

nnhelpers im Typo3 Repository (TER) veröffentlicht

Seit heute ist einer unser wichtigster „Mitarbeiter“ im TER und unterstützt Euch ab sofort bei Euren eigenen Projekten! Selbstverständlich vollkommen ehrenamtlich und ohne Berechnung 😉

Die Extension „nnhelpers“, eine Sammlung extrem hilfreicher Methoden und ViewHelper, die wir ausnahmslos jeden Tag bei der Extension-Entwicklung in Typo3 brauchen.

Hier geht es zur Extension im Repository von Typo3:
https://extensions.typo3.org/extension/nnhelpers

Die Dokumentation dazu gibt es hier:
https://labor.99grad.de/typo3-docs/nnhelpers/de/
https://labor.99grad.de/typo3-docs/nnhelpers/en/

typeNum in Link funktioniert nicht. Typo3 type removed from URL (Typo3 9, Typo3 10)

Problem nach dem Update auf Typo3 9 oder 10:

Beim Erstellen eines Links über den ViewHelper f:link.page, f:uri.page, f:link.typolink, f:uri.typolink etc. wird der typeNum-Parameter ignoriert.
Auch beim Versuch, type manuell über additionalParams zu übergeben, erscheint kein &type=123456 in der generierten URL:

<f:link.page pageType="123456" additionalParams="{s:1,p:2,type:123456}" pageUid="3"<

wird zu:
https://www.domain.de/?s=1&p=2

Der type=123456-Parameter wird einfach entfernt.

Lösung:
Die site-yaml Konfiguration prüfen und darauf achten, dass ein routeEnhancer für den Page-Type definiert wurde!


routeEnhancers:
  PageTypeSuffix:
    type: PageType
    default: '/'
    index: ''
    map:
      beispiel: 123456

Hier – als kleiner Spickzettel – noch mal die TypoScript-Setup zum simplen Aufruf einer Methode über typeNum.

Mit entsprechender Einstellung im TypoScript und der Site-Konfiguration (yaml) wird die angegebene Methode beim Aufruf der URL https://www.beispiel.de/?type=123456 ausgeführt.


beispiel = PAGE
beispiel {
  typeNum = 123456
  config {
    disableAllHeaderCode = 1
    xhtml_cleaning = 0
    admPanel = 0
    additionalHeaders = Content-type: text/plain
    no_cache = 1
    contentObjectExceptionHandler = 0
  }
  10 = USER
  10.userFunc = Pfad\Zu\Deinem\Script->methodenName
}

Für Suchmaschinen: Typo3 Typolink ignores type-Parameter, typeNum not working Typo3, typeNum funktioniert nicht, type wird aus URL entfernt beim ViewHelper. Typoscript mit page-type wird nicht ausgeführt. Problem typeNum in Link-URL.

typo3 templavoila – Sie haben nicht die nötigen Rechte, um diese Änderung durchzuführen.

Sie haben nicht die nötigen Rechte, um diese Änderung durchzuführen

Bei einer Typo3 6.2 Installation mit TemplaVoila 1.9.2 standen wir vor einem fast unlösbaren Rätsel: Aus unerfindlichem Grund funktionierte das Anlegen von neuen Inhaltselementen nicht mehr – weder in der Listenansicht noch in der TV-Seitenansicht. Nach Auswahl eines Contentelementes aus dem Wizard brach Typo3 mit der Fehlermeldung „Sie haben nicht die nötigen Rechte, um diese Änderung durchzuführen.“ ab. Und das, obwohl wir als Admin eingeloggt waren.

Vor wenigen Tagen noch lief alles problemlos und wir hatten kein Update eingespielt.
Im Netz findet mal viele Lösungen dazu, am häufigsten den Hinweis, dass ein Feld in der Datenbank-Tabelle pages fehlen würde.

t3ver_swapmode ist es nicht!

Die allgemein anerkannte Lösung, folgenden MySQL-Befehl zum Einfügen der fehlenden Spalte auszuführen, funktionierte aber leider nicht:

ALTER table pages add(`t3ver_swapmode` tinyint(4) NOT NULL DEFAULT '0');

MariaDB hat Probleme mit unmaskiertem „recursive“ von Typo3

Nach langem Suchen: Der Host hatte die Datenbank auf MariaDB umgestellt. MariaDB hat ein reserviertes Keyword recursive – das blöderweise auch ein Spaltenname in der Tabelle tt_content ist. Beim Generieren der INSERT-Query in /typo3/sysext/core/Classes/Database/DatabaseConnection.php maskiert Typo3 allerdings die Feldnamen nicht durch backticks – es steht als INSERT INTO tt_content (recursive, ...) statt INSERT INTO tt_content(`recursive`, ...).

Die Query bricht mit der Fehlermeldung „SQL Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ‚recursive,menu_type,list_type,table_bgColor,table_border,table_cellspacing,ta…‘ at line 1“ ab.

Patch und Lösung

Das Problem wurde in einer späteren Version von Typo3 gefixed. Für Typo3 < 6 kann es mithilfe dieser Patches zum Laufen gebracht werden: https://forge.typo3.org/attachments/35472?utf8=%E2%9C%93&type=sbs

Schritt für Schritt Anleitung

  1. Die Datei /typo3/sysext/core/Classes/Database/DatabaseConnection.php bearbeiten
  2. Folgende Zeilen suchen und ersetzen:

Suche nach:
$query = 'INSERT INTO ' . $table . ' (' . implode(',', array_keys($fields_values)) . ') VALUES ' . '(' . implode(',', $fields_values) . ')';
Ersetzen mit:
$query = 'INSERT INTO ' . $table . ' (`' . implode('`,`', array_keys($fields_values)) . '`) VALUES ' . '(' . implode(',', $fields_values) . ')';


Suche nach:
$query = 'INSERT INTO ' . $table . ' (' . implode(', ', $fields) . ') VALUES ';
Ersetzen mit:
$query = 'INSERT INTO ' . $table . ' (`' . implode('`,`', $fields) . '`) VALUES ';


Suche nach:
$fields[] = $k . '=' . $v;
Ersetzen mit:
$fields[] = '`' . $k . '`=' . $v;


Suche nach:
$queryParts[] = $table . '.' . implode(($like . ' OR ' . $table . '.'), $fields) . $like;
Ersetzen mit:
$queryParts[] = $table . '.`' . implode(('`' . $like . ' OR ' . $table . '.`'), $fields) . '`' .$like;


Für Suchmaschinen: In TYPO3 erscheint beim Anlegen eines Inhaltselements folgende Fehlermeldung von TemplaVoila: „Sie haben nicht die nötigen Rechte, um diese Änderung durchzuführen.“ Probleme Typo3 Zugriffsrechte Backend Templavoila trotz Admin-Rechten. TemplaVoila funktioniert unter MariaDB nicht mehr. Typo3 Problem MariaDB INSERT Query. TemplaVoila forbidden new content element, SQL Error: You have an error in your SQL syntax. MariaDB.

Typo3 Extension „mask“: Mehrere templateRootPaths angeben

Kleines, sehr hilfreiches TypoScript für die wunderbare Extension „mask“ (Typo3).

Nicht selten kommt man an das Problem, mehr als nur einen Pfad zu seinen Mask-Templates definieren zu wollen. Die Extension-Konfiguration im Backend lässt leider nur jeweils einen einzigen Pfad für Folder for Content Fluid Templates (with trailing slash) frontend.content (folder), Folder for Content Fluid Layouts (with trailing slash) frontend.layouts (folder) und Folder for Content Fluid Partials (with trailing slash) frontend.partials (folder) zu.

Das Problem lässt sich aber im TypoScript-Setup lösen.

Ab Typo3 9 LTS


lib.maskContentElement {
  templateRootPaths {
    // 10 wird von mask gesetzt, abhängig von der EXT-Konfiguration im Backend
    20 = EXT:deine/ext/Resources/Private/Templates/
  }
  partialRootPaths {
    // 10 wird von mask gesetzt, abhängig von der EXT-Konfiguration im Backend
    20 = EXT:deine/ext/Resources/Private/Partials/
  }
  layoutRootPaths {
    // 10 wird von mask gesetzt, abhängig von der EXT-Konfiguration im Backend
    20 = EXT:deine/ext/Resources/Private/Layouts/
  }
}

Bis einschließlich Typo3 8 LTS


lib.tx_mask {
  templateRootPaths {
    // 10 wird von mask gesetzt, abhängig von der EXT-Konfiguration im Backend
    20 = EXT:deine/ext/Resources/Private/Templates/
  }
  partialRootPaths {
    // 10 wird von mask gesetzt, abhängig von der EXT-Konfiguration im Backend
    20 = EXT:deine/ext/Resources/Private/Partials/
  }
  layoutRootPaths {
    // 10 wird von mask gesetzt, abhängig von der EXT-Konfiguration im Backend
    20 = EXT:deine/ext/Resources/Private/Layouts/
  }
}

Wenn mir jetzt noch jemand sagen könnte, wie man die mask.json in mehrere Dateien bekommt… dann würde ich den Entwicklern von mask eine Bierkasten-Abo schenken 🙂

Für Suchmaschinen: Typo3 Extension Mask. Mehrere Pfade zu Templates und templateRootPaths für mask typo3. mask typo3 multiple templateRootPaths. Zusätzliche Pfade für Mask-Templates per TypoScript angeben.

Typo3 tx_form: Empfänger im Frontend auswählbar machen. Eigenes Formular-Element und eigener Finisher

Im Formular-Editor des FormFrameworks von Typo3 soll man verschiedene Empfänger angeben können. Im Frontend erscheint eine Dropdown-Liste der Empfänger. Der User kann eine Zieladresse aus dem Dropdown auswählen und so bestimmen, wer die Mail erhält.


Anleitung im Backend FormEditor

  • Neues Formular-Element in Formular einfügen
  • Im Formular-Element eine Liste der möglichen Empfänger angeben. Pro Zeile ein Empfänger in dieser Schreibweise: email@adresse.de;Empfängername
  • Neuen Finisher für das Formular auswählen

1. Wähle einen Namen für Deine Extension

Wenn Du die beiden Felder hier ausfüllst, wird im Quelltext alles automatisch für Dich ersetzt:



2. Eine Extension anlegen

(oder eine vorhandene nutzen)

Wir ignorieren mal alle Normen und halten es simpel. Folgende Dateien und Struktur brauchst Du in Deiner Extension.

Ersetze nng durch den Ordner-Namen Deiner Extension, in der Du arbeitest.
Ersetze das Wort RecipientSelector im Dateinamen durch Deinen Bezeichner.

nng
│
├── Classes
│   ├── Finishers
│   │   └── RecipientSelectorFinisher.php
│   ├── ViewHelpers
│   │   └── RecipientSelectorViewHelper.php
├── Configuration
│   ├── TypoScript
│   │   └── setup.txt
│   └── Yaml
│       ├── BaseSetup.yaml
│       ├── FormEditorSetup.yaml
│       └── FormEngineSetup.yaml
├── Resources
│   ├── Private
│   │   └── FormFramework
│   │       └── Frontent
│   │           └── Partials
│   │               └── RecipientSelector.html
│   └── Public
│       ├── Icons
│       │   └── RecipientSelector-icon.svg
│       └── JavaScript
│           └── Backend
│               └── FormEditor
│                   └── RecipientSelectorViewModel.js
├── ext_emconf.php
├── ext_localconf.php
└── ext_typoscript_setup.txt

3. Inhalte der Dateien

Folgender Code muss in die einzelnen Dateien:


Configuration/TypoScript/setup.txt


# Einstellungen für das Frontend
plugin.tx_form.settings.yamlConfigurations {
   153450370601 = EXT:nng/Configuration/Yaml/FormEditorSetup.yaml
}
# Einstellungen für das Backend
module.tx_form.settings.yamlConfigurations {
   153450370601 = EXT:nng/Configuration/Yaml/FormEditorSetup.yaml
   153450370602 = EXT:nng/Configuration/Yaml/FormEngineSetup.yaml
}

Configuration/Yaml/FormEditorSetup.yaml


# ---------------------------------------------------------------------------
# Diese YAML wird im Backend UND Frontend geladen.
# 

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:
          formElementsDefinition:

            # ---------------------------------------------------------
            # Registrierung zusätzlicher Fluid-Pfade

            Form:
              renderingOptions:
                templateRootPaths:
                  125: 'EXT:nng/Resources/Private/FormFramework/Frontend/Templates/'
                partialRootPaths:
                  125: 'EXT:nng/Resources/Private/FormFramework/Frontend/Partials/'
                layoutRootPaths:
                  125: 'EXT:nng/Resources/Private/FormFramework/Frontend/Layouts/'

            # ---------------------------------------------------------
            # Registrierung des neuen Formular-Elementes

            RecipientSelector:
              __inheritances:
                10: 'TYPO3.CMS.Form.mixins.formElementMixins.FormElementMixin'
              formEditor:
                label: 'Empfänger-Auswahl'
                group: custom
                groupSorting: 1000
                iconIdentifier: 'nng-selectrecipient'
                predefinedDefaults:
                  properties:
                    nng-recipients: ''
                editors:
                  300:
                    identifier: 'nng-recipients'
                    templateName: 'Inspector-TextareaEditor'
                    label: 'Empfänger-Liste'
                    propertyPath: 'properties.nng-recipients'

          # ---------------------------------------------------------
          # Registrierung des Finishers
          
          finishersDefinition:
            RecipientSelector:
              __inheritances:
                10: 'TYPO3.CMS.Form.mixins.finishersEmailMixin'
              implementationClassName: 'Nng\RecipientSelector\Finishers\RecipientSelectorFinisher'
              FormEngine:
                label: "C E-Mail an Empfänger aus Auswahl (nng)"
              formEditor:
                iconIdentifier: 'nng-selectrecipient'
                label: 'D E-Mail an Empfänger aus Auswahl (nng)'
                predefinedDefaults:
                  options:
                    pageUid: ''
                    additionalParameters: ''



Configuration/Yaml/FormEngineSetup.yaml



# ---------------------------------------------------------------------------
# Diese YAML wird NUR im Backend geladen.
# 

TYPO3:
  CMS:
    Form:
      prototypes:
        standard:

          # -------------------------------------------------------
          # JS für Formular-Element im Backend bereitstellen
          
          formEditor:

            dynamicRequireJsModules:
              additionalViewModelModules:
                125: 'TYPO3/CMS/Nnsite/Backend/FormEditor/RecipientSelectorViewModel'

            formEditorPartials:
              FormElement-RecipientSelector: 'Stage/SimpleTemplate'

          formElementsDefinition:
            Form:
              # -------------------------------------------------------
              # Backend: Definition des Finishers

              formEditor:
                editors:
                  # 900 ist fix. Hier werden die Finisher definiert.
                  900:
                    selectOptions:
                      125:
                        value: 'RecipientSelector'
                        label: 'E-Mail an Empfänger aus Auswahl (nnsite)'

                propertyCollections:
                  finishers:
                    125:
                      identifier: 'RecipientSelector'
                      editors:
                        __inheritances:
                          10: 'TYPO3.CMS.Form.mixins.formElementMixins.BaseCollectionEditorsMixin'
                        100:
                          label: "E-Mail an Empfänger aus Auswahl (nnsite)"
                        120:
                          identifier: 'fieldSubject'
                          templateName: 'Inspector-TextEditor'
                          label: 'Betreff'
                          propertyPath: 'options.fieldSubject'
                          enableFormelementSelectionButton: true
                          propertyValidators:
                            10: 'NotEmpty'
                        500:
                          identifier: 'fieldFromEmail'
                          templateName: 'Inspector-TextEditor'
                          label: 'formEditor.elements.Form.finisher.EmailToSender.editor.senderAddress.label'
                          propertyPath: 'options.fieldFromEmail'
                          enableFormelementSelectionButton: true
                          propertyValidatorsMode: 'OR'
                          propertyValidators:
                            10: 'NaiveEmail'
                            20: 'FormElementIdentifierWithinCurlyBracesExclusive'
                          fieldExplanationText: 'formEditor.elements.Form.finisher.EmailToSender.editor.senderAddress.fieldExplanationText'
                        600:
                          identifier: 'fieldFromName'
                          templateName: 'Inspector-TextEditor'
                          label: 'formEditor.elements.Form.finisher.EmailToSender.editor.senderName.label'
                          propertyPath: 'options.fieldFromName'
                          enableFormelementSelectionButton: true
                          propertyValidators:
                            10: 'FormElementIdentifierWithinCurlyBracesInclusive'
                          fieldExplanationText: 'formEditor.elements.Form.finisher.EmailToSender.editor.senderName.fieldExplanationText'

Classes/ViewHelpers/RecipientSelectorViewHelper.html

Das eigentliche Fluid-Template für die Ausgabe im Frontend.


<?php

namespace Extname\RecipientSelector\ViewHelpers;

use \TYPO3\CMS\Extbase\Utility\DebuggerUtility;
use \TYPO3\CMS\Core\Utility\GeneralUtility;


class RecipientSelectorViewHelper extends \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper {


    /**
     * @var boolean
     */
    protected $escapeChildren = false;


    /**
     * @var boolean
     */
    protected $escapeOutput = false;
    
    
    /**
	 * Initialize arguments.
	 *
	 * @return void
	 */
	public function initializeArguments() {
		parent::initializeArguments();
    	$this->registerArgument('str', 'string', 'Text', false);
	}
	
	
	/**
	 * 
	 *
	 * @throws \TYPO3\CMS\Fluid\Core\ViewHelper\Exception
	 * @return string Rendered tag
	 */
	public function render() {
		$str = $this->arguments['str'] ?: $this->renderChildren();
		$rows = explode("\n", $str);
		foreach ($rows as $k => $row) {
			$parts = explode(";", $row);
			$rows[$k] = ['key' => $k, 'email' => $parts[0], 'label'=>$parts[1]];
		}
		return $rows;
    }


}

Classes/Finishers/RecipientSelectorFinisher.php


<?php
namespace Extname\RecipientSelector'\Finishers;
 
use TYPO3\CMS\Core\Utility\GeneralUtility;	 
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\PathUtility;

class RecipientSelectorFinisher extends \TYPO3\CMS\Form\Domain\Finishers\EmailFinisher {
    
    /**
	 * Finisher
     * 
	 */
	protected function executeInternal()
	{
		$formRuntime = $this->finisherContext->getFormRuntime();
		$formValues = $this->finisherContext->getFormValues();

		$fromEmail = $this->parseOption('fieldFromEmail');
		$fromName = $this->parseOption('fieldFromName');
		$subjectEmail = $this->parseOption('fieldSubject');

		$recipientElement = false;

		// Finde das ERSTE Formular-Element, in dem die Empfänger definiert wurden
		foreach ($formValues as $k => $val) {
			$element = $formRuntime->getFormDefinition()->getElementByIdentifier($k);
			if ($element->getType() == 'RecipientSelector') {
				$recipientElement = [
					'key' 		=> $k,
					'element' 	=> $element,
					'value'		=> $val,
				];
			}
		}

		if (!$recipientElement) {
			die('Finisher ist im Formular angegeben, aber Formular-Element für Auswahl des Empfängers fehlt.');
		}

		$recipients = explode("\n", $recipientElement['element']->getProperties()['nnsite-recipients'] ?? '');

		foreach ($recipients as $n=>$recipient) {
			$parts = explode(";", $recipient);
			$recipients[$n] = [
				'key' => $n,
				'label' => $parts[1],
				'email' => $parts[0],
			];
		}

		if (!$recipients) {
			die('Keine Zieladressen im Formular angegeben.');
		}

		$recipientName = $recipients[$recipientElement['value']]['label'] ?? '';
		$recipientEmail = $recipients[$recipientElement['value']]['email'] ?? '';

		if (!$recipients) {
			die('Zieladresse nicht bekannt.');
		}

		$this->setOption('templateName', 'html');
		$this->setOption('subject', $subjectEmail);
		$this->setOption('recipientName', $recipientName);
		$this->setOption('recipientAddress', $recipientEmail);
		$this->setOption('senderName', $fromName);
		$this->setOption('senderAddress', $fromEmail);

		parent::executeInternal();
	}

}

Resources/Private/FormFramework/Frontend/Partials/RecipientSelector.html

Das eigentliche Fluid-Template für die Ausgabe im Frontend.

<div>
	<h2>{element.properties.elementDescription}</h2>
	<f:form.textfield
		property="{element.identifier}"
		id="{element.uniqueIdentifier}"
		class="{element.properties.elementClassAttribute} form-control form-field"
		errorClass="{element.properties.elementErrorClassAttribute}"
		additionalAttributes="{formvh:translateElementProperty(element:element, property: 'fluidAdditionalAttributes')}"
	/>	
</div>

Resources/Public/Icons/RecipientSelector-icon.svg

Ein simples, quadratisches SVG-Icon für die Darstellung im Backend.


Resources/Public/JavaScript/Backend/FormEditor/RecipientSelectorViewmodel.js


// Resources/Public/JavaScript/Backend/FormEditor/RecipientSelectorViewmodel.js

define([
    'jquery',
    'TYPO3/CMS/Form/Backend/FormEditor/Helper'
], function ($, Helper) {
    'use strict';

    return (function ($, Helper) {

        /**
         * @private
         *
         * @var object
         */
        var _formEditorApp = null;

        /**
         * @private
         *
         * @return object
         */
        function getFormEditorApp() {
            return _formEditorApp;
        };

        /**
         * @private
         *
         * @return object
         */
        function getPublisherSubscriber() {
            return getFormEditorApp().getPublisherSubscriber();
        };

        /**
         * @private
         *
         * @return object
         */
        function getUtility() {
            return getFormEditorApp().getUtility();
        };

        /**
         * @private
         *
         * @param object
         * @return object
         */
        function getHelper() {
            return Helper;
        };

        /**
         * @private
         *
         * @return object
         */
        function getCurrentlySelectedFormElement() {
            return getFormEditorApp().getCurrentlySelectedFormElement();
        };

        /**
         * @private
         *
         * @param mixed test
         * @param string message
         * @param int messageCode
         * @return void
         */
        function assert(test, message, messageCode) {
            return getFormEditorApp().assert(test, message, messageCode);
        };

        /**
         * @private
         *
         * @return void
         * @throws 1491643380
         */
        function _helperSetup() {
            assert('function' === $.type(Helper.bootstrap),
                'The view model helper does not implement the method "bootstrap"',
                1491643380
            );
            Helper.bootstrap(getFormEditorApp());
        };

        /**
         * @private
         *
         * @return void
         */
        function _subscribeEvents() {
            /**
             * @private
             *
             * @param string
             * @param array
             *              args[0] = formElement
             *              args[1] = template
             * @return void
             */
            getPublisherSubscriber().subscribe('view/stage/abstract/render/template/perform', function (topic, args) {
                if (args[0].get('type') === 'RecipientSelector') {
                    getFormEditorApp().getViewModel().getStage().renderSimpleTemplateWithValidators(args[0], args[1]);
                }
            });
        };

        /**
         * @public
         *
         * @param object formEditorApp
         * @return void
         */
        function bootstrap(formEditorApp) {
            _formEditorApp = formEditorApp;
            _helperSetup();
            _subscribeEvents();
        };

        /**
         * Publish the public methods.
         * Implements the "Revealing Module Pattern".
         */
        return {
            bootstrap: bootstrap
        };
    })($, Helper);
});

ext_emconf.php

Der übliche Code, um eine Extension zu registrieren – falls nicht schon vorhanden.

<?php
$EM_CONF[$_EXTKEY] = [
    'title' => 'Meine Extension',
    'description' => '',
    'category' => 'plugin',
    'author' => 'David Bascom',
    'author_email' => 'deine@email.de',
    'state' => 'stable',
    'internal' => '',
    'uploadfolder' => '1',
    'createDirs' => '',
    'clearCacheOnLoad' => 0,
    'version' => '0.1',
    'constraints' => [
        'depends' => [
            'typo3' => '8.7.0',
        ],
        'conflicts' => [],
        'suggests' => [],
    ],
];

ext_localconf.php

Registriert das Icon

<?php 
if (TYPO3_MODE === 'BE') {
    /** @var \TYPO3\CMS\Core\Imaging\IconRegistry $iconRegistry */
    $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class);
    $iconRegistry->registerIcon(
        'RecipientSelector-icon',
        \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
        ['source' => 'EXT:nng/Resources/Public/Icons/RecipientSelector-icon.svg']
    );
}

ext_typoscript_setup.txt

Lädt das TypoScript, ohne das im Root-Template die „Enthält Erweiterungen“ gewählt werden müssen.

<INCLUDE_TYPOSCRIPT: source="FILE:EXT:nng/Configuration/Typoscript/setup.txt">

jQuery rebuild data – data-Attribut erneut parsen / einlesen

Bei der Arbeit mit VueJS in Kombination mit jQuery kommt es häufig zu dem Problem, dass VueJS das DOM oder sogar das data-Attribut eines Elementes ändert, aber jQuery noch per jQuery.data() auf alte Daten zugreift.

Hintergrund: Beim Initialisieren der Elemente cached jQuery die data-*-Attribute und speichert sie in einer internen Variable, um später schneller darauf zugreifen zu können. Eine Änderung der data-*-Attribute über VueJS – oder bereits eine Änderung der Reihenfolge der Elemente im DOM – führt zu einer falschen Zuordnung zwischen Element und dem data-Cache von jQuery.

Dieses kleine Plugin löst das Problem: Nach einer Änderung des DOM über VueJS kann einfach $('.element').rebuildData(); aufgerufen werden, um alle data-Attribute des Elementes und rekursiv aller Unterknoten erneut zu parsen und in jQuery.data() zu speichern.

Weiterlesen

Spamfreie E-Mails mit eigenem Server versenden

Wer eine eigene Domain besitzt, steht vor der Entscheidung die E-Mails auf ein Konto bei Anbietern wie Google Mail, GMX oder Web.de weiterzuleiten oder ein sich beim Hoster ein eigenes Konto für die E-Mail-Adresse einzurichten. Für letzteren Fall bieten Hosting-Provider wie Host Europe in der Regel einen eigenen Mailserver an, der bereits über eine entsprechende vorgefertigte Konfiguration verfügt. So werden auch passende DNS-Einträge der Domain beim Hoster automatisch gesetzt. Ganz bequem also. Doch sobald ein eigener Server im Einsatz ist, wird es möglicherweise schon etwas kniffeliger, um mit versendeten E-Mails nicht im Spam-Order des Empfängers zu landen. Wie ist dies also einzustellen?

Weiterlesen