Thursday, June 23, 2022

Using TYPO3 FluidEmail in CLI context

Since TYPO3 10.4 it is possible to use TYPO3\CMS\Core\Mail\FluidEmail to send emails with body content rendered by Fluid. This all works fine as long as you work in frontend or backend context, but when you use FluidEmail in CLI context (e.g. in a symfony console command), you will run into some problems. Since no real server request object is available, TYPO3 is not able to construct URIs using Fluid ViewHelpers nor will the Image ViewHelper render working absolute image links. Especially the last problem (non working website logo in rendered FluidEmail default template) motivated me to dig deeper into the topic. 

Below follows a summary of things you should consider when working with FluidEmail (or Fluid StandaloneView) in CLI context.

All Fluid ViewHelpers in the namespace f:uri or f:link do not work by default

The ViewHelpers depend on either a TypoScriptFrontendController object or the global TYPO3_REQUEST variable being available. Both are not in CLI context. 

One workaround for this problem is to create an instance of $GLOBALS['TYPO3_REQUEST'] manually in your symfony console command as shown below:


$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId(1);
$request = (new ServerRequest())
    ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
    ->withAttribute('site', $site);
$GLOBALS['TYPO3_REQUEST'] = $request;

Note, that the site object is fetched by the uid of page 1. This must be kept in mind, when you have a TYPO3 website with multiple sites defined.

Link created with f:link.action must always contain the extensionName

If the previous workaround is used, it is mandatory to set the "extensionName" argument in the f:link.action or f:uri.action ViewHelper.

Create links manually

An alternative to the previous example, where the $GLOBALS['TYPO3_REQUEST'] object is created manually, is to create all links manually used in the FluidEmail template in the symfony console command. This can done as shown below:


$arguments = [
    'tx_sfeventmgt_pieventdetail' => [
        'action' => 'detail',
        'controller' => 'Event',
        'event' => 25,
    ],
];

$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId(1);
$eventLink = (string)$site->getRouter()->generateUri(22, $arguments);

Note again, that the site object is fetched by the uid of page 1. The variable $eventLink can then be passed to the view and be used as href attribute for a-tags.

Image ViewHelper creates broken URLs

When you try to render an image with an absolute path using the f:image ViewHelper, the resulting link will be broken. Example:


<f:image absolute="1" src="EXT:sf_event_mgt/Resources/Public/Icons/Extension.svg" width="100" />

In CLI context this will create an img tag as shown below


<img src="http://./typo3/sysext/core/bin/typo3conf/ext/sf_event_mgt/Resources/Public/Icons/Extension.svg" width="100" height="104" alt="">

In order to create a workaround for the problem, I found 2 different approaches.

Workaround 1

FluidEmail has the function setRequest() which allows to set a ServerRequest object for the view. 


$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId(1);

$normalizedParams = new NormalizedParams(
    [
        'HTTP_HOST' => $site->getBase()->getHost(),
        'HTTPS' => $site->getBase()->getScheme() === 'https' ? 'on' : 'off',
    ],
    $systemConfiguration ?? $GLOBALS['TYPO3_CONF_VARS']['SYS'],
    '',
    ''
);

$request = (new ServerRequest())
    ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
    ->withAttribute('normalizedParams', $normalizedParams)
    ->withAttribute('site', $site);
    
$email->setRequest($request);

By assigning the request object to the view, the variable {normalizedParams.siteUrl} is now also available in CLI context resulting in the TYPO3 logo being shown again in the default email layout. 

Workaround 2

The f:image ViewHelper will create relative links to images when the argument "absolute" is not used. So basically just the baseUri is missing. In the previous workaround {normalizedParams.siteUrl} was used to add the baseUri, but instead it is also possible to assign the sites baseUri as Fluid variable as shown below:


$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId(1);
$email->assign('baseUri', (string)$site->getBase());

In the template the full path to the image can now be created as following:


<img src="{baseUri}{f:uri.image(src: 'EXT:sf_event_mgt/Resources/Public/Icons/Extension.svg', width: '100')}" width="100" />

Ensure to set the correct application context in CLI

Whenever the site object is fetched using the SiteFinder, the resulting base is resolved respecting TYPO3s application context. By default this is "production". If you e.g. have a testing or staging environment and use one of the described workarounds, ensure to set the application context as environment variable as shown below:

export TYPO3_CONTEXT=Development


Friday, January 21, 2022

How to use multiple SMTP accounts in one TYPO3 installation

When TYPO3 is used to serve multiple websites in one installation, it may sometimes be required to configure multiple SMTP accounts in order to send emails from TYPO3 (e.g. mailforms or notifications) to different recipients. This may especially be important, when the recipient mailserver has a strict spam filter or when the domain uses a SPF, DKIM or DMARC and the mailserver only accepts emails from thrusted sources.

In TYPO3 you can configure one global SMTP server in LocalConfiguration.php by using the following settings:

$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport'] = 'smtp';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server'] = 'your.mailserver.tld';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt'] = true;
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username'] = 'username';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password'] = 'password';
$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'email@your.mailserver.tld';

This setting however reflects to any hosted website in your TYPO3 installation and the email-server for typo3-website1.tld may possible not accept emails with a sender from the domain typo3-website2.tld.

In order to provide multiple SMTP servers for different websites in a TYPO3 installation, I configure different SMTP servers in AdditionalConfiguration.php 


if (($_SERVER['SERVER_NAME'] ?? '') === 'typo3-website1.tld') {
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server'] = 'mail.typo3-website1.tld';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt'] = true;
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username'] = 'username';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password'] = 'password';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'email@typo3-website1.tld';
}

if (($_SERVER['SERVER_NAME'] ?? '') === 'typo3-website2.tld') {
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_server'] = 'mail.typo3-website2.tld';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_encrypt'] = true;
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_username'] = 'username';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['transport_smtp_password'] = 'password';
    $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'email@typo3-website2.tld';
}

Since AdditionalConfiguration.php is evaluated on every request, TYPO3 will conditionally use the email settings depending on the $_SERVER['SERVER_NAME'] variable.

Note, that this solution only applies to web requests and does not work in CLI context (e.g. scheduler task).

Wednesday, January 19, 2022

TYPO3 - Multiple dynamic parameters for a typolink using a custom userFunc

I often use the TYPO3 linkHandler to enable the possibility for editors to create direct links to records from within the CKEditor in TYPO3 backend. This is all well documented and easy to configure using the RecordLinkHandler, as long as the resulting link only contains one dynamic parameter. But sometimes it may be required to have multiple dynamic parameters for the resulting link. In this case you may need to create a userFunc for the typolink function in order to create a custom configuration which uses multiple dynamic parameters.

Requirement

Let us assume, you have an event which has multiple event registrations. Registrations are listed in the detail view of an event and each registration is shown as an accordion item with a unique ID in markup. Now you want to create a link to an event and set a link anchor to a specific registration. The resulting URL should be as shown below:

https://www.cool-events.tld/events/my-first-event#registration-1

Calling the URL will open the event detail page and scroll down the the HTML element with the ID "registration-1".

Note: This is just an example, which also can be achieved without a custom userFunc. Goal of this article is to demonstrate how to use a userFunc for typolink.

Solution

In order to archive the requirement, first a linkHandler Page TSConfig must be created as shown below:


TCEMAIN.linkHandler {
    event {
        handler = TYPO3\CMS\Recordlist\LinkHandler\RecordLinkHandler
        label = Event Registration
        configuration {
            table = tx_sfeventmgt_domain_model_registration
        }
    }
}

Next, the TypoScript for the link generation is added. 


config {
  recordLinks {
    registration {
      typolink {
        parameter = 1
        userFunc = DERHANSEN\SfEventMgt\UserFunc\TypoLink->createEventLink
        userFunc {
          eventUid = TEXT
          eventUid.data = field:event
          registrationUid = TEXT
          registrationUid.data = field:uid
        }
      }
    }
  }
}

Finally a custom userFunc needs to be created which renders the A-tag for the link.


<?php

declare(strict_types=1);

namespace DERHANSEN\SfEventMgt\UserFunc;

use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

class TypoLink
{
    private const EVENT_DETAILPID = 22;

    public ContentObjectRenderer $cObj;

    public function createEventLink(array $content, array $config): string
    {
        $eventUid = $this->cObj->cObjGetSingle($config['eventUid'], $config['eventUid.']);
        $registrationUid = $this->cObj->cObjGetSingle($config['registrationUid'], $config['registrationUid.']);

        // Link parameters (can also contain multiple dynamic parameters)
        $parameters = [
            'tx_sfeventmgt_pieventdetail' => [
                'controller' => 'Event',
                'action' => 'detail',
                'event' => $eventUid,
            ]
        ];

        $link = $this->cObj->typoLink($this->cObj->lastTypoLinkResult->getLinkText(), [
            'parameter' => self::EVENT_DETAILPID,
            'additionalParams' => '&' . http_build_query($parameters),
            'section' => 'registration-' . $registrationUid,
            'returnLast' => 'url',
        ]);

        return '<a href="' . $link . '">';
    }
}

The most important part is, that the custom userFunc must only return the opening A-tag. In the userFunc, it is basically possible to construct the resulting link however you want. In the example above, 2 dynamic parameters are used in the function ($eventUid and $registrationUid). It is of course also possible to e.g. do dynamic database lookups in the function to fetch other dynamic parameters required for link construction.


Wednesday, December 29, 2021

How to manually create the default crop variant string for an imported image in TYPO3 CMS

When data in TYPO3 is created automatically (e.g. through a custom API or by an import script), it is very common, that also new files (especially images) are imported. TYPO3 has the well documented FAL (File Abstraction Layer), which provides an API for common tasks. 

One typical task is to import an image to FAL and next creating a file reference for the imported image to a record (e.g. event record of ext:sf_event_mgt). Such a task is easy to implement in a custom extension when you follow the example documentation from the TYPO3 FAL API. This all works fine as described, as long as the referenced image does not use Crop Variants. For image fields, where crop variants are configured, no crop variants will be created for imported images and as a result, TYPO3 will always use the aspect ratio of the imported image no matter which crop variant is configured for output.

TYPO3 internals

Available crop variants for image fields are defined in TCA. When an editor adds an image to a record in the TYPO3 backend, TYPO3 will automatically calculate the default crop variants. The result is saved to the table sys_file_reference in the field crop as a JSON encoded string like the shown example crop variant string below:

{"heroimage":{"cropArea":{"x":0,"y":0.23,"width":1,"height":0.54},"selectedRatio":"16:9","focusArea":null},"teaserimage":{"cropArea":{"x":0,"y":0.14,"width":1,"height":0.72},"selectedRatio":"4:3","focusArea":null}}

This process if performed in ImageManipulationElement. When an editor for example edits a record with an imported image and opens an image inline element, then the ImageManipulationElement is rendered and the default crop variant string is calculated for the imported image and saved, as soon as the editor saves the record.

Manually calculating the default crop variant string

In order to manually calculate the default crop variant string in an image import process, it is required to extract 2 code snippets from the ImageManipulationElement, since both are not public available:

  1. The default crop configuration - see code
  2. The function populateConfiguration - see code
Both is extracted to a class named CropVariantUtility and an additional public function is added which returns the crop variant string for a given file as shown below:

/**
 * Returns a crop variant string to be used in sys_file_reference field "crop" for the given file and table/fieldname
 *
 * @param File $file
 * @param string $tableName
 * @param string $fieldname
 * @return string
 * @throws InvalidConfigurationException
 */
public static function getCropVariantString(File $file, string $tableName, string $fieldname): string
{
    $config = $GLOBALS['TCA'][$tableName]['columns'][$fieldname]['config']['overrideChildTca']['columns']['crop']['config'];
    $cropVariants = self::populateConfiguration($config);
    $cropVariantCollection = CropVariantCollection::create('', $cropVariants['cropVariants']);
    if (!empty($file->getProperty('width'))) {
        $cropVariantCollection = $cropVariantCollection->applyRatioRestrictionToSelectedCropArea($file);
    }

    return (string)$cropVariantCollection;
}

The whole class is available in this gist.

Finally the new CropVariantUtility can be used in a file import routine as shown in the example below:


protected function addFileToEvent(File $file, int $eventUid): void
{
    $eventRecord = BackendUtility::getRecord(self::EVENT_TABLE, $eventUid);

    $fileReferenceUid = StringUtility::getUniqueId('NEW');

    $dataMap = [];
    $dataMap['sys_file_reference'][$fileReferenceUid] = [
        'table_local' => 'sys_file',
        'tablenames' => self::EVENT_TABLE,
        'uid_foreign' => $eventUid,
        'uid_local' => $file->getUid(),
        'fieldname' => 'image',
        'pid' => $eventRecord['pid'],
        'show_in_views' => 0,
        'crop' => CropVariantUtility::getCropVariantString($file, self::EVENT_TABLE, 'image'),
    ];

    $dataMap[self::EVENT_TABLE][$eventUid] = [
        'image' => $fileReferenceUid
    ];

    $this->dataHandler->start($dataMap, []);
    $this->dataHandler->process_datamap();
}

The example code will create a new file reference for the given file object for the table  self::EVENT_TABLE (tx_sfeventmgt_domain_model_event in this case) and the given event UID. The usage of the new CropVariantUtility ensures, that the new file relation has a default crop variant string and configured crop variants can directly be used for imported images.

Sunday, October 17, 2021

TYPO3 extension "Event management and registration" version 6.0 for TYPO3 11.5 LTS released

I am really proud and happy to announce, that the new version 6.0. of my TYPO3 extension "Event management and registration" (GitHub / TYPO3 Extension Repository) is now fully compatible with TYPO3 11.5 LTS including support for PHP 7.4 and 8.0.

Originally I wanted to release this version of the extension on the same day as TYPO3 11.5 LTS got released, but I decided to consider all possible deprecations from TYPO3 core and also to reactor the extension to support strict types and strict properties where ever possible. All in all, my planned 6 days for a TYPO3 11.5 LTS compatible version resulted in more than 10 days of work. Well, not all changes were required for the release (e.g. removal of switchableControllerActions), but the code base is now better than before and I'm happy with all improvements that made its way into the extension.

Changes in more than 145 commits

The most important changes are of course those who break existing functionality. Although the new version contains 7 breaking changes and much of the codebase has been changed too, existing users can migrate to the new version with the least possible manual work. 

The list below contains some of the important changes:

  • The extension uses strict types and typed properties wherever possible
  • switchableControllerActions have been removed. The extension now has 7 individual plugins instead. An update wizard will migrate existing plugins and settings.
  • Data Transfer Objects do not extend AbstractEntity any more
  • Native TYPO3 pagination API support for event list view
  • Captcha integration has been refactored to support both reCaptcha or hCaptcha
  • All possible TYPO3 core deprecations have been handled
All breaking changes have been documented in detail in the release notes, so existing users know which parts of the extension need further attention when updating.



Sunday, August 15, 2021

"Unterminated nested statement!" using TYPO3 rector

TYPO3 rector is a really helpful application when it comes to TYPO3 major updates. It helps you to identify and refactor TYPO3 deprecations in custom extensions and can save hours of manual refactoring. I use TYPO3 rector quite a lot and stumbled across the following error recently.

"Unterminated nested statement!"







This message is not really helpful, so I digged deeper into the problem. The "Parser.php" throwing the exception is located in "helmich/typo3-typoscript-parser" package, so I first thought that there was a problem with the TypoScript in the desired extension, but after checking ever line manually, I could not find any error. 

It came out, that the extension I wanted to process by TYPO3 rector had a "node_modules" folder, which contained a lot of Typescript (not TypoScript) files. Those files where obviously parsed by the TypoScript parser resulting in the shown error message. After removing (excluding should also work) the "node_modules" folder, everything worked as expected.

If you like rector and/or TYPO3 rector, please consider to support the authors.
 

Friday, June 4, 2021

How to use constructor dependency injection in a XCLASSed TYPO3 class

Some time ago in needed to extend an Extbase controller in TYPO3 10.4 which used dependency injection through constructor injection. So I used XCLASS to extend the original controller and added an own constructor which added an additional dependency, but this obviously did not work out properly, since the constructor was always called with the amount of arguments from the original class.

Later I created this issue on TYPO3 forge in order to find out if this is a bug/missing feature or if I missed something in my code. In order to demonstrate the problem, I created this small demo extension which basically just extended a TYPO3 core class using XCLASS and just days later, a solution for the issue was provided.

The solution is pretty simple and you just have to ensure to add a reference to the extended class in the Services.yaml file of the extending extension.

Example:


  TYPO3\CMS\Belog\Controller\BackendLogController: '@Derhansen\XclassDi\Controller\ExtendedBackendLogController'

The complete Services.yaml file can be found here.

Thanks a lot to Lukas Niestroj, who pointed out the solution to the problem.