[!!!][FEATURE] Enhanced new MetaTag API 38/56738/46
authorRichard Haeser <richard@maxserv.com>
Thu, 19 Apr 2018 13:43:50 +0000 (15:43 +0200)
committerAndreas Wolf <andreas.wolf@typo3.org>
Fri, 4 May 2018 14:42:44 +0000 (16:42 +0200)
It is now possible to use a new MetaTag API having managers to handle
specific Meta Tag "families" like OpenGraph.

You can use the API by TypoScript and from PHP. It will use the
DependencyOrderingService to define the order of the managers.

Besides the managers shipped by core, you can also add and register
your own managers.

Resolves: #81464
Releases: master
Change-Id: I64f349c32e542087597f033eb48e4d218a5cd53c
Reviewed-on: https://review.typo3.org/56738
Reviewed-by: Mathias Schreiber <mathias.schreiber@typo3.com>
Tested-by: Mathias Schreiber <mathias.schreiber@typo3.com>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Reviewed-by: Richard Haeser <richard@maxserv.com>
Tested-by: Richard Haeser <richard@maxserv.com>
Reviewed-by: Andreas Wolf <andreas.wolf@typo3.org>
Tested-by: Andreas Wolf <andreas.wolf@typo3.org>
17 files changed:
typo3/sysext/core/Classes/MetaTag/AbstractMetaTagManager.php [new file with mode: 0644]
typo3/sysext/core/Classes/MetaTag/EdgeMetaTagManager.php [new file with mode: 0644]
typo3/sysext/core/Classes/MetaTag/GenericMetaTagManager.php [new file with mode: 0644]
typo3/sysext/core/Classes/MetaTag/Html5MetaTagManager.php [new file with mode: 0644]
typo3/sysext/core/Classes/MetaTag/MetaTagManagerInterface.php [new file with mode: 0644]
typo3/sysext/core/Classes/MetaTag/MetaTagManagerRegistry.php [new file with mode: 0644]
typo3/sysext/core/Classes/MetaTag/OpenGraphMetaTagManager.php [new file with mode: 0644]
typo3/sysext/core/Classes/MetaTag/TwitterCardMetaTagManager.php [new file with mode: 0644]
typo3/sysext/core/Classes/Page/PageRenderer.php
typo3/sysext/core/Documentation/Changelog/master/Feature-81464-AddAPIForMetaTagManagement.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
typo3/sysext/core/Tests/Unit/MetaTag/GenericMetaTagManagerTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/MetaTag/MetaTagManagerRegistryTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/MetaTag/OpenGraphMetaTagManagerTest.php [new file with mode: 0644]
typo3/sysext/core/ext_localconf.php
typo3/sysext/frontend/Classes/Page/PageGenerator.php
typo3/sysext/frontend/Tests/Unit/Page/PageGeneratorTest.php

diff --git a/typo3/sysext/core/Classes/MetaTag/AbstractMetaTagManager.php b/typo3/sysext/core/Classes/MetaTag/AbstractMetaTagManager.php
new file mode 100644 (file)
index 0000000..b3efe93
--- /dev/null
@@ -0,0 +1,320 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+abstract class AbstractMetaTagManager implements MetaTagManagerInterface, SingletonInterface
+{
+    /**
+     * The default attribute that defines the name of the property
+     *
+     * This creates tags like <meta name="" /> by default
+     *
+     * @var string
+     */
+    protected $defaultNameAttribute = 'name';
+
+    /**
+     * The default attribute that defines the content
+     *
+     * This creates tags like <meta content="" /> by default
+     *
+     * @var string
+     */
+    protected $defaultContentAttribute = 'content';
+
+    /**
+     * Set if by default it is possible to have multiple occurrences of properties of this manager
+     *
+     * @var bool
+     */
+    protected $defaultAllowMultipleOccurrences = false;
+
+    /**
+     * The separator to define subproperties like og:image:width
+     *
+     * @var string
+     */
+    protected $subPropertySeparator = ':';
+
+    /**
+     * Array of properties that can be handled by this manager
+     *
+     * Example:
+     *
+     * $handledProperties = [
+     *       'og:title' => [],
+     *       'og:image' => [
+     *          'allowMultipleOccurrences' => true,
+     *          'allowedSubProperties' => [
+     *              'url',
+     *              'secure_url',
+     *              'type',
+     *              'width',
+     *              'height',
+     *              'alt'
+     *          ]
+     *       ],
+     *       'og:locale' => [
+     *          'allowedSubProperties' => [
+     *             'alternate' => [
+     *                'allowMultipleOccurrences' => true
+     *             ]
+     *          ]
+     *       ]
+     *];
+     *
+     * @var array
+     */
+    protected $handledProperties = [];
+
+    /**
+     * Array of properties that are set by the manager
+     *
+     * @var array
+     */
+    protected $properties = [];
+
+    /**
+     * Gets instance of the manager
+     *
+     * @return MetaTagManagerInterface
+     */
+    public static function getInstance(): MetaTagManagerInterface
+    {
+        return GeneralUtility::makeInstance(__CLASS__);
+    }
+
+    /**
+     * Add a property
+     *
+     * @param string $property Name of the property
+     * @param string $content Content of the property
+     * @param array $subProperties Optional subproperties
+     * @param bool $replace Replace the currently set value
+     * @param string $type Optional type of property (name, property, http-equiv)
+     *
+     * @throws \UnexpectedValueException
+     */
+    public function addProperty(string $property, string $content, array $subProperties = [], bool $replace = false, string $type = '')
+    {
+        $property = strtolower($property);
+
+        if (isset($this->handledProperties[$property])) {
+            $subPropertiesArray = [];
+            foreach ($subProperties as $subPropertyKey => $subPropertyValue) {
+                if (isset($this->handledProperties[$property]['allowedSubProperties'][$subPropertyKey])) {
+                    $subPropertiesArray[$subPropertyKey] = (is_array($subPropertyValue)) ? $subPropertyValue : [$subPropertyValue];
+                }
+            }
+            if (!isset($this->properties[$property]) || empty($this->properties[$property])) {
+                $this->properties[$property][] = ['content' => $content, 'subProperties' => $subPropertiesArray];
+            } else {
+                if ($replace === true) {
+                    $this->removeProperty($property, $type);
+                    $this->properties[$property][] = ['content' => $content, 'subProperties' => $subPropertiesArray];
+                    return;
+                }
+
+                if (isset($this->handledProperties[$property]['allowMultipleOccurrences']) &&
+                    (bool)$this->handledProperties[$property]['allowMultipleOccurrences']
+                ) {
+                    $this->properties[$property][] = ['content' => $content, 'subProperties' => $subPropertiesArray];
+                }
+            }
+        } else {
+            // Check if there is an allowed subproperty that can handle the given property
+            foreach ($this->handledProperties as $handledProperty => $handledPropertyConfig) {
+                if (!isset($handledPropertyConfig['allowedSubProperties'])) {
+                    continue;
+                }
+                foreach ((array)$handledPropertyConfig['allowedSubProperties'] as $allowedSubProperty => $allowedSubPropertyConfig) {
+                    $propertyKey = is_array($allowedSubPropertyConfig) ? $allowedSubProperty : $allowedSubPropertyConfig;
+
+                    if ($property !== $handledProperty . $this->subPropertySeparator . $propertyKey ||
+                        !isset($this->properties[$handledProperty])
+                    ) {
+                        continue;
+                    }
+
+                    $propertyArrayKeys = array_keys($this->properties[$handledProperty]);
+                    $lastIndex = end($propertyArrayKeys);
+
+                    if (!isset($this->properties[$handledProperty][$lastIndex]['subProperties'][$propertyKey])) {
+                        $this->properties[$handledProperty][$lastIndex]['subProperties'][$propertyKey][] = $content;
+                    } else {
+                        if ($replace === true) {
+                            unset($this->properties[$handledProperty][$lastIndex]['subProperties'][$propertyKey]);
+                            $this->properties[$handledProperty][$lastIndex]['subProperties'][$propertyKey][] = $content;
+                            return;
+                        }
+
+                        if (is_array($allowedSubPropertyConfig) &&
+                            isset($allowedSubPropertyConfig['allowMultipleOccurrences']) &&
+                            (bool)$allowedSubPropertyConfig['allowMultipleOccurrences']
+                        ) {
+                            $this->properties[$handledProperty][$lastIndex]['subProperties'][$propertyKey][] = $content;
+                        }
+                    }
+
+                    return;
+                }
+            }
+
+            throw new \UnexpectedValueException(
+                sprintf('This MetaTagManager can\'t handle property "%s"', $property),
+                1524209729
+            );
+        }
+    }
+
+    /**
+     * Returns an array with all properties that can be handled by this manager
+     *
+     * @return array
+     */
+    public function getAllHandledProperties(): array
+    {
+        return $this->handledProperties;
+    }
+
+    /**
+     * Get a specific property that is set before
+     *
+     * @param string $property Name of the property
+     * @param string $type Optional type of property (name, property, http-equiv)
+     * @return array
+     */
+    public function getProperty(string $property, string $type = ''): array
+    {
+        $property = strtolower($property);
+
+        if (isset($this->properties[$property])) {
+            return $this->properties[$property];
+        }
+
+        return [];
+    }
+
+    /**
+     * Render a meta tag for a specific property
+     *
+     * @param string $property Name of the property
+     * @return string
+     */
+    public function renderProperty(string $property): string
+    {
+        $property = strtolower($property);
+        $metaTags = [];
+
+        $nameAttribute = $this->defaultNameAttribute;
+        if (isset($this->handledProperties[$property]['nameAttribute'])
+            && !empty((string)$this->handledProperties[$property]['nameAttribute'])) {
+            $nameAttribute = (string)$this->handledProperties[$property]['nameAttribute'];
+        }
+
+        $contentAttribute = $this->defaultContentAttribute;
+        if (isset($this->handledProperties[$property]['contentAttribute'])
+            && !empty((string)$this->handledProperties[$property]['contentAttribute'])) {
+            $contentAttribute = (string)$this->handledProperties[$property]['contentAttribute'];
+        }
+
+        if ($nameAttribute && $contentAttribute) {
+            foreach ($this->getProperty($property) as $propertyItem) {
+                $metaTags[] = '<meta ' .
+                    htmlspecialchars($nameAttribute) . '="' . htmlspecialchars($property) . '" ' .
+                    htmlspecialchars($contentAttribute) . '="' . htmlspecialchars($propertyItem['content']) . '" />';
+
+                if (!count($propertyItem['subProperties'])) {
+                    continue;
+                }
+                foreach ($propertyItem['subProperties'] as $subProperty => $subPropertyItems) {
+                    foreach ($subPropertyItems as $subPropertyItem) {
+                        $metaTags[] = '<meta ' .
+                            htmlspecialchars($nameAttribute) . '="' . htmlspecialchars($property . $this->subPropertySeparator . $subProperty) . '" ' .
+                            htmlspecialchars($contentAttribute) . '="' . htmlspecialchars((string)$subPropertyItem) . '" />';
+                    }
+                }
+            }
+        }
+
+        return implode(PHP_EOL, $metaTags);
+    }
+
+    /**
+     * Render all registered properties of this manager
+     *
+     * @return string
+     */
+    public function renderAllProperties(): string
+    {
+        $metatags = [];
+        foreach (array_keys($this->properties) as $property) {
+            $metatags[] = $this->renderProperty($property);
+        }
+
+        return implode(PHP_EOL, $metatags);
+    }
+
+    /**
+     * Remove one property from the MetaTagManager
+     * If there are multiple occurrences of a property, they all will be removed
+     *
+     * @param string $property
+     * @param string $type
+     */
+    public function removeProperty(string $property, string $type = '')
+    {
+        $property = strtolower($property);
+
+        unset($this->properties[$property]);
+    }
+
+    /**
+     * Unset all properties of this MetaTagManager
+     */
+    public function removeAllProperties()
+    {
+        $this->properties = [];
+    }
+
+    /**
+     * Check if this manager can handle the given property
+     *
+     * @param string $property Name of property to check (eg. og:title)
+     * @return bool
+     */
+    public function canHandleProperty(string $property): bool
+    {
+        if (isset($this->handledProperties[$property])) {
+            return true;
+        }
+
+        foreach ($this->handledProperties as $handledProperty => $handledPropertyConfig) {
+            foreach ((array)$handledPropertyConfig['allowedSubProperties'] as $allowedSubProperty => $allowedSubPropertyConfig) {
+                $propertyKey = is_array($allowedSubPropertyConfig) ? $allowedSubProperty : $allowedSubPropertyConfig;
+                if ($property === $handledProperty . $this->subPropertySeparator . $propertyKey) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/typo3/sysext/core/Classes/MetaTag/EdgeMetaTagManager.php b/typo3/sysext/core/Classes/MetaTag/EdgeMetaTagManager.php
new file mode 100644 (file)
index 0000000..ddf7bdf
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class EdgeMetaTagManager extends AbstractMetaTagManager
+{
+    protected $handledProperties = [
+        'x-ua-compatible' => ['nameAttribute' => 'http-equiv']
+    ];
+}
diff --git a/typo3/sysext/core/Classes/MetaTag/GenericMetaTagManager.php b/typo3/sysext/core/Classes/MetaTag/GenericMetaTagManager.php
new file mode 100644 (file)
index 0000000..f064b41
--- /dev/null
@@ -0,0 +1,174 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+class GenericMetaTagManager implements MetaTagManagerInterface, SingletonInterface
+{
+    /**
+     * The separator to define subproperties like og:image:width
+     *
+     * @var string
+     */
+    protected $subPropertySeparator = ':';
+
+    /**
+     * Array of properties that are set by the manager
+     *
+     * @var array
+     */
+    protected $properties = [];
+
+    /**
+     * Add a property (including subProperties)
+     *
+     * @param string $property
+     * @param string $content
+     * @param array $subProperties
+     * @param string $type
+     */
+    public function addProperty(string $property, string $content, array $subProperties = [], bool $replace = false, string $type = 'name')
+    {
+        $property = strtolower($property);
+        $type = strtolower($type) ?: 'name';
+
+        if ($replace) {
+            $this->removeProperty($property, $type);
+        }
+
+        $this->properties[$property][$type][] = [
+            'content' => $content,
+            'subProperties' => $subProperties
+        ];
+    }
+
+    /**
+     * Get the data of a specific property
+     *
+     * @param string $property
+     * @param string $type
+     * @return array
+     */
+    public function getProperty(string $property, string $type = 'name'): array
+    {
+        $property = strtolower($property);
+        $type = strtolower($type) ?: 'name';
+
+        if (!empty($this->properties[$property][$type])) {
+            return $this->properties[$property][$type];
+        }
+        return [];
+    }
+
+    /**
+     * Returns an array with all properties that can be handled by this manager
+     * @return array
+     */
+    public function getAllHandledProperties(): array
+    {
+        return [];
+    }
+
+    /**
+     * Render all registered properties of this manager
+     *
+     * @return string
+     */
+    public function renderAllProperties(): string
+    {
+        $metatags = [];
+        foreach (array_keys($this->properties) as $property) {
+            $metatags[] = $this->renderProperty($property);
+        }
+
+        return implode(PHP_EOL, $metatags);
+    }
+
+    /**
+     * Render a specific property including subproperties of that property
+     *
+     * @param string $property
+     * @return string
+     */
+    public function renderProperty(string $property): string
+    {
+        $property = strtolower($property);
+
+        $metaTags = [];
+        foreach ((array)$this->properties[$property] as $type => $propertyItems) {
+            foreach ($propertyItems as $propertyItem) {
+                $metaTags[] = '<meta ' .
+                    htmlspecialchars($type) . '="' . htmlspecialchars($property) . '" ' .
+                    'content="' . htmlspecialchars($propertyItem['content']) . '" />';
+
+                if (!count($propertyItem['subProperties'])) {
+                    continue;
+                }
+                foreach ($propertyItem['subProperties'] as $subProperty => $value) {
+                    $metaTags[] = '<meta ' .
+                        htmlspecialchars($type) . '="' . htmlspecialchars($property . $this->subPropertySeparator . $subProperty) . '" ' .
+                        'content="' . htmlspecialchars((string)$value) . '" />';
+                }
+            }
+        }
+
+        return implode(PHP_EOL, $metaTags);
+    }
+
+    /**
+     * Remove one property from the MetaTagManager
+     * If there are multiple occurrences of a property, they all will be removed
+     *
+     * @param string $property
+     * @param string $type
+     */
+    public function removeProperty(string $property, string $type = '')
+    {
+        if (!empty($type)) {
+            unset($this->properties[$property][$type]);
+        } else {
+            unset($this->properties[$property]);
+        }
+    }
+
+    /**
+     * Unset all properties
+     */
+    public function removeAllProperties()
+    {
+        $this->properties = [];
+    }
+
+    /**
+     * @return MetaTagManagerInterface
+     */
+    public static function getInstance(): MetaTagManagerInterface
+    {
+        return GeneralUtility::makeInstance(__CLASS__);
+    }
+
+    /**
+     * @param string $property
+     * @return bool
+     */
+    public function canHandleProperty(string $property): bool
+    {
+        return true;
+    }
+}
diff --git a/typo3/sysext/core/Classes/MetaTag/Html5MetaTagManager.php b/typo3/sysext/core/Classes/MetaTag/Html5MetaTagManager.php
new file mode 100644 (file)
index 0000000..c558ac1
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class Html5MetaTagManager extends AbstractMetaTagManager
+{
+    /**
+     * Array of properties that can be handled by this manager
+     *
+     * @var array
+     */
+    protected $handledProperties = [
+        'application-name' => [],
+        'author' => [],
+        'description' => [],
+        'generator' => [],
+        'keywords' => [],
+        'referrer' => [],
+        'content-language' => [
+            'nameAttribute' => 'http-equiv'
+        ],
+        'content-type' => [
+            'nameAttribute' => 'http-equiv'
+        ],
+        'default-style' => [
+            'nameAttribute' => 'http-equiv'
+        ],
+        'refresh' => [
+            'nameAttribute' => 'http-equiv'
+        ],
+        'set-cookie' => [
+            'nameAttribute' => 'http-equiv'
+        ],
+        'content-security-policy' => [
+            'nameAttribute' => 'http-equiv'
+        ],
+        'viewport' => [],
+        'robots' => [],
+        'expires' => [
+            'nameAttribute' => 'http-equiv'
+        ],
+        'cache-control' => [
+            'nameAttribute' => 'http-equiv'
+        ],
+        'pragma' => [
+            'nameAttribute' => 'http-equiv'
+        ]
+    ];
+}
diff --git a/typo3/sysext/core/Classes/MetaTag/MetaTagManagerInterface.php b/typo3/sysext/core/Classes/MetaTag/MetaTagManagerInterface.php
new file mode 100644 (file)
index 0000000..ff75542
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+interface MetaTagManagerInterface
+{
+    /**
+     * Gets instance of the manager
+     *
+     * @return MetaTagManagerInterface
+     */
+    public static function getInstance(): MetaTagManagerInterface;
+
+    /**
+     * Add a property
+     *
+     * @param string $property
+     * @param string $content
+     * @param array $subProperties
+     * @param string $type
+     */
+    public function addProperty(string $property, string $content, array $subProperties = [], bool $replace = false, string $type = '');
+
+    /**
+     * Get a specific property that is set before
+     *
+     * @param string $property
+     * @param string $type
+     * @return array
+     */
+    public function getProperty(string $property, string $type = ''): array;
+
+    /**
+     * Check if this manager can handle the given property
+     *
+     * @param string $property
+     * @return bool
+     */
+    public function canHandleProperty(string $property): bool;
+
+    /**
+     * Returns an array with all properties that can be handled by the manager
+     *
+     * @return array
+     */
+    public function getAllHandledProperties(): array;
+
+    /**
+     * Render all registered properties of this manager
+     *
+     * @return string
+     */
+    public function renderAllProperties(): string;
+
+    /**
+     * Render a meta tag for a specific property
+     *
+     * @param string $property
+     * @return string
+     */
+    public function renderProperty(string $property): string;
+
+    /**
+     * Remove one property from the MetaTagManager
+     * If there are multiple occurrences of a property, they all will be removed
+     *
+     * @param string $property
+     * @param string $type
+     */
+    public function removeProperty(string $property, string $type = '');
+
+    /**
+     * Unset all properties of this MetaTagManager
+     */
+    public function removeAllProperties();
+}
diff --git a/typo3/sysext/core/Classes/MetaTag/MetaTagManagerRegistry.php b/typo3/sysext/core/Classes/MetaTag/MetaTagManagerRegistry.php
new file mode 100644 (file)
index 0000000..033812f
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+class MetaTagManagerRegistry implements SingletonInterface
+{
+    protected $registry = [];
+
+    public function __construct()
+    {
+        $this->registry['generic'] = [
+            'module' => GenericMetaTagManager::class
+        ];
+    }
+
+    /**
+     * Returns instance of this class
+     *
+     * @return MetaTagManagerRegistry
+     */
+    public static function getInstance()
+    {
+        return GeneralUtility::makeInstance(self::class);
+    }
+
+    /**
+     * Add a MetaTagManager to the registry
+     *
+     * @param string $name
+     * @param string $className
+     * @param array $before
+     * @param array $after
+     */
+    public function registerManager(string $name, string $className, array $before = ['generic'], array $after = [])
+    {
+        if (!count($before)) {
+            $before[] = 'generic';
+        }
+
+        $this->registry[$name] = [
+            'module' => $className,
+            'before' => $before,
+            'after' => $after
+        ];
+    }
+
+    /**
+     * Get the MetaTagManager for a specific property
+     *
+     * @param string $property
+     * @return MetaTagManagerInterface
+     */
+    public function getManagerForProperty(string $property): MetaTagManagerInterface
+    {
+        foreach ($this->getAllManagers() as $manager) {
+            if ($manager->canHandleProperty($property)) {
+                return $manager;
+            }
+        }
+
+        // Just a fallback because the GenericMetaTagManager is also registered in the list of MetaTagManagers
+        return GenericMetaTagManager::getInstance();
+    }
+
+    /**
+     * Get an array of all registered MetaTagManagers
+     *
+     * @return MetaTagManagerInterface[]
+     */
+    public function getAllManagers(): array
+    {
+        $orderedManagers = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies(
+            $this->registry
+        );
+
+        $managers = [];
+        foreach ($orderedManagers as $managerConfiguration) {
+            if (class_exists($managerConfiguration['module'])) {
+                $managers[] = GeneralUtility::makeInstance($managerConfiguration['module']);
+            }
+        }
+
+        return $managers;
+    }
+
+    /**
+     * Remove all registered MetaTagManagers
+     */
+    public function removeAllManagers()
+    {
+        unset($this->registry);
+    }
+}
diff --git a/typo3/sysext/core/Classes/MetaTag/OpenGraphMetaTagManager.php b/typo3/sysext/core/Classes/MetaTag/OpenGraphMetaTagManager.php
new file mode 100644 (file)
index 0000000..9cf4ed4
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class OpenGraphMetaTagManager extends AbstractMetaTagManager
+{
+    /**
+     * The default attribute that defines the name of the property
+     *
+     * This creates tags like <meta property="" /> by default
+     *
+     * @var string
+     */
+    protected $defaultNameAttribute = 'property';
+
+    /**
+     * Array of properties that can be handled by this manager
+     *
+     * @var array
+     */
+    protected $handledProperties = [
+        'og:type' => [],
+        'og:title' => [],
+        'og:description' => [],
+        'og:site_name' => [],
+        'og:url' => [],
+        'og:audio' => [],
+        'og:video' => [],
+        'og:determiner' => [],
+        'og:locale' => [
+            'allowedSubProperties' => [
+                'alternate' => [
+                    'allowMultipleOccurrences' => true
+                ],
+            ]
+        ],
+        'og:image' => [
+            'allowMultipleOccurrences' => true,
+            'allowedSubProperties' => [
+                'url' => [],
+                'secure_url' => [],
+                'type' => [],
+                'width' => [],
+                'height' => [],
+                'alt' => [],
+            ]
+        ]
+    ];
+}
diff --git a/typo3/sysext/core/Classes/MetaTag/TwitterCardMetaTagManager.php b/typo3/sysext/core/Classes/MetaTag/TwitterCardMetaTagManager.php
new file mode 100644 (file)
index 0000000..7be0bc6
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+class TwitterCardMetaTagManager extends AbstractMetaTagManager
+{
+    /**
+     * Array of properties that can be handled by this manager
+     *
+     * @var array
+     */
+    protected $handledProperties = [
+        'twitter:card' => [],
+        'twitter:site' => [
+            'allowedSubProperties' => [
+                'id',
+            ]
+        ],
+        'twitter:creator' => [
+            'allowedSubProperties' => [
+                'id',
+            ]
+        ],
+        'twitter:description' => [],
+        'twitter:title' => [],
+        'twitter:image' => [
+            'allowedSubProperties' => [
+                'alt',
+            ]
+        ],
+        'twitter:player' => [
+            'allowedSubProperties' => [
+                'width',
+                'height',
+                'stream',
+            ]
+        ],
+        'twitter:app' => [
+            'allowedSubProperties' => [
+                'name:iphone',
+                'id:iphone',
+                'url:iphone',
+                'name:ipad',
+                'id:ipad',
+                'url:ipad',
+                'name:googleplay',
+                'id:googleplay',
+                'url:googleplay',
+            ]
+        ],
+    ];
+}
index 4a319b5..10781f7 100644 (file)
@@ -19,6 +19,7 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\CMS\Core\Localization\LocalizationFactory;
+use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -949,9 +950,11 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
      * @param string $type The type of the meta tag. Allowed values are property, name or http-equiv
      * @param string $name The name of the property to add
      * @param string $content The content of the meta tag
+     * @param array $subProperties Subproperties of the meta tag (like e.g. og:image:width)
+     * @param bool $replace Replace earlier set meta tag
      * @throws \InvalidArgumentException
      */
-    public function setMetaTag(string $type, string $name, string $content)
+    public function setMetaTag(string $type, string $name, string $content, array $subProperties = [], $replace = true)
     {
         /**
          * Lowercase all the things
@@ -964,7 +967,10 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
                 1496402460
             );
         }
-        $this->metaTagsByAPI[$type][$name] = $content;
+
+        $metaTagManagerRegistry = MetaTagManagerRegistry::getInstance();
+        $manager = $metaTagManagerRegistry->getManagerForProperty($name);
+        $manager->addProperty($name, $content, $subProperties, $replace, $type);
     }
 
     /**
@@ -982,11 +988,16 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
          */
         $type = strtolower($type);
         $name = strtolower($name);
-        if (isset($this->metaTagsByAPI[$type], $this->metaTagsByAPI[$type][$name])) {
+
+        $metaTagManagerRegistry = MetaTagManagerRegistry::getInstance();
+        $manager = $metaTagManagerRegistry->getManagerForProperty($name);
+        $propertyContent = $manager->getProperty($name, $type);
+
+        if (!empty($propertyContent[0])) {
             return [
                 'type' => $type,
                 'name' => $name,
-                'content' => $this->metaTagsByAPI[$type][$name]
+                'content' => $propertyContent[0]['content']
             ];
         }
         return [];
@@ -1005,7 +1016,10 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
          */
         $type = strtolower($type);
         $name = strtolower($name);
-        unset($this->metaTagsByAPI[$type][$name]);
+
+        $metaTagManagerRegistry = MetaTagManagerRegistry::getInstance();
+        $manager = $metaTagManagerRegistry->getManagerForProperty($name);
+        $manager->removeProperty($name, $type);
     }
 
     /**
@@ -1640,10 +1654,11 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
      */
     protected function renderMetaTagsFromAPI()
     {
+        $metaTagManagerRegistry = MetaTagManagerRegistry::getInstance();
         $metaTags = [];
-        foreach ($this->metaTagsByAPI as $metaTagType => $type) {
-            foreach ($type as $metaType => $content) {
-                $metaTags[] = '<meta ' . htmlspecialchars($metaTagType) . '="' . htmlspecialchars($metaType) . '" content="' . htmlspecialchars($content) . '"' . $this->endingSlash . '>';
+        foreach ($metaTagManagerRegistry->getAllManagers() as $manager) {
+            if ($properties = $manager->renderAllProperties()) {
+                $metaTags[] = $properties;
             }
         }
         return $metaTags;
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-81464-AddAPIForMetaTagManagement.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-81464-AddAPIForMetaTagManagement.rst
new file mode 100644 (file)
index 0000000..81493b6
--- /dev/null
@@ -0,0 +1,102 @@
+.. include:: ../../Includes.txt
+
+=================================================
+Feature: #81464 - Add API for meta tag management
+=================================================
+
+See :issue:`81464`
+
+Description
+===========
+
+In order to have the possibility to set metatags in a flexible (but regulated way), a new Meta Tag API is introduced.
+
+The API uses `MetaTagManagers` to manage the tags for a "family" of meta tags. The core e.g. ships an OpenGraph MetaTagManager that is responsible for all OpenGraph tags. In addition to the MetaTagManagers included in the core, you can also register your own `MetaTagManager` in the `MetaTagManagerRegistry`.
+
+Using the Meta Tag API
+======================
+
+To use the API, first get the right `MetaTagManager` for your tag from the `MetaTagManagerRegistry`. You can use that manager to add your meta tag; see the example below for the `og:title` meta tag.
+
+.. code-block:: php
+
+    $metaTagManager = \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::getInstance()->getManagerForProperty('og:title');
+    $metaTagManager->addProperty('og:title', 'This is the OG title from a controller');
+
+This code will result in a `<meta property="og:title" content="This is the OG title from a controller" />` tag in frontend.
+
+If you need to specify sub-properties, e.g. `og:image:width`, you can use the following code:
+
+.. code-block:: php
+
+    $metaTagManager = \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::getInstance()->getManagerForProperty('og:image');
+    $metaTagManager->addProperty('og:image', '/path/to/image.jpg', ['width' => 400, 'height' => 400]);
+
+You can also remove a specific property:
+
+.. code-block:: php
+
+    $metaTagManager = \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::getInstance()->getManagerForProperty('og:title');
+    $metaTagManager->removeProperty('og:title');
+
+Or remove all previously set meta tags of a specific manager:
+
+.. code-block:: php
+
+    $metaTagManager = \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::getInstance()->getManagerForProperty('og:title');
+    $metaTagManager->removeAllProperties();
+
+Creating your own MetaTagManager
+================================
+
+If you need to specify the settings and rendering of a specific meta tag (for example when you want to make it possible to have multiple occurences of a specific tag), you can create your own `MetaTagManager`. This MetaTagManager should implement `\TYPO3\CMS\Core\MetaTag\MetaTagManagerInterface`.
+
+To use the manager, you have to register it in `ext_localconf.php`:
+
+.. code-block:: php
+
+    $metaTagManagerRegistry = \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::getInstance();
+    $metaTagManagerRegistry->registerManager(
+        'custom',
+        \Some\CustomExtension\MetaTag\CustomMetaTagManager::class
+    );
+
+Registering a `MetaTagManager` works with the `DependencyOrderingService`. So you can also specify the priority of the manager by setting the third (before) and fourth (after) parameter of the method. If you for example want to implement your own `OpenGraphMetaTagManager`, you can use the following code:
+
+.. code-block:: php
+
+    $metaTagManagerRegistry = \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::getInstance();
+    $metaTagManagerRegistry->registerManager(
+        'myOwnOpenGraphManager',
+        \Some\CustomExtension\MetaTag\MyOpenGraphMetaTagManager::class,
+        ['opengraph']
+    );
+
+This will result in `MyOpenGraphMetaTagManager` having a higher priority and it will first check if your own manager can handle the tag before it checks the default manager provided by the core.
+
+TypoScript and PHP
+==================
+
+You can set your meta tags by TypoScript and PHP (for example from plugins). First the meta tags from content (plugins) will be handled. After that the meta tags defined in TypoScript will be handled.
+
+It is possible to override earlier set meta tags by TypoScript if you explicitly say this should happen. Therefore the `meta.*.replace` option was introduced. It is a boolean flag with these values:
+
+* `1`: The meta tag set by TypoScript will replace earlier set meta tags
+* `0`: (default) If the meta tag is not set before, the meta tag will be created. If it is already set, it will ignore the meta tag set by TypoScript.
+
+.. code-block:: typoscript
+
+    page.meta {
+        og:site_name = TYPO3
+        og:site_name.attribute = property
+        og:site_name.replace = 1
+    }
+
+When you set the property replace to 1 at the specific tag, the tag will replace tags that are set from plugins.
+
+Impact
+======
+
+By using the new API it is not possible to have duplicate metatags, unless this is explicitly allowed. If you use custom meta tags and want to have multiple occurrences of the same meta tag, you have to create your own `MetaTagManager`.
+
+.. index:: ext:core
index 3a728ca..9e270f9 100644 (file)
@@ -54,6 +54,8 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
         $subject->setMetaTag('name', 'author', 'foobar');
         $subject->setMetaTag('http-equiv', 'refresh', '5');
         $subject->setMetaTag('name', 'DC.Author', '<evil tag>');
+        $subject->setMetaTag('property', 'og:image', '/path/to/image1.jpg', [], false);
+        $subject->setMetaTag('property', 'og:image', '/path/to/image2.jpg', [], false);
 
         // Unset meta tag
         $subject->setMetaTag('NaMe', 'randomTag', 'foobar');
@@ -112,6 +114,8 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
         $this->assertNotContains('<meta name="randomtag" content="foobar">', $renderedString);
         $this->assertNotContains('<meta name="randomtag" content="foobar" />', $renderedString);
         $this->assertContains('<meta name="generator" content="TYPO3 CMS" />', $renderedString);
+        $this->assertContains('<meta property="og:image" content="/path/to/image1.jpg" />', $renderedString);
+        $this->assertContains('<meta property="og:image" content="/path/to/image2.jpg" />', $renderedString);
     }
 
     /**
diff --git a/typo3/sysext/core/Tests/Unit/MetaTag/GenericMetaTagManagerTest.php b/typo3/sysext/core/Tests/Unit/MetaTag/GenericMetaTagManagerTest.php
new file mode 100644 (file)
index 0000000..499a220
--- /dev/null
@@ -0,0 +1,256 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\MetaTag\GenericMetaTagManager;
+
+class GenericMetaTagManagerTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function checkIfGetAllHandledPropertiesReturnsNonEmptyArray()
+    {
+        $manager = new GenericMetaTagManager();
+        $handledProperties = $manager->getAllHandledProperties();
+
+        $this->assertEmpty($handledProperties);
+    }
+
+    /**
+     * @test
+     */
+    public function checkIfMethodCanHandlePropertyAlwaysReturnsTrue()
+    {
+        $manager = new GenericMetaTagManager();
+        $this->assertTrue($manager->canHandleProperty('custom-meta-tag'));
+        $this->assertTrue($manager->canHandleProperty('description'));
+        $this->assertTrue($manager->canHandleProperty('og:title'));
+    }
+
+    /**
+     * @dataProvider propertiesProvider
+     *
+     * @test
+     */
+    public function checkIfPropertyIsStoredAfterAddingProperty($property, $expected, $expectedRenderedTag)
+    {
+        $manager = new GenericMetaTagManager();
+        $manager->addProperty(
+            $property['property'],
+            $property['content'],
+            (array)$property['subProperties'],
+            $property['replace'],
+            $property['type']
+        );
+
+        $this->assertEquals($expected, $manager->getProperty($property['property'], $property['type']));
+        $this->assertEquals($expectedRenderedTag, $manager->renderProperty($property['property']));
+    }
+
+    /**
+     * @test
+     */
+    public function checkRenderAllPropertiesRendersCorrectMetaTags()
+    {
+        $properties = [
+            [
+                'property' => 'description',
+                'content' => 'This is a description',
+                'subProperties' => [],
+                'replace' => false,
+                'type' => ''
+            ],
+            [
+                'property' => 'og:image',
+                'content' => '/path/to/image',
+                'subProperties' => [
+                    'width' => 400
+                ],
+                'replace' => false,
+                'type' => 'property'
+            ],
+            [
+                'property' => 'og:image:height',
+                'content' => '200',
+                'subProperties' => [],
+                'replace' => false,
+                'type' => 'property'
+            ],
+            [
+                'property' => 'twitter:card',
+                'content' => 'This is the Twitter card',
+                'subProperties' => [],
+                'replace' => false,
+                'type' => ''
+            ],
+            [
+                'property' => 'og:image',
+                'content' => '/path/to/image2',
+                'subProperties' => [],
+                'replace' => true,
+                'type' => 'property'
+            ],
+        ];
+
+        $manager = new GenericMetaTagManager();
+        foreach ($properties as $property) {
+            $manager->addProperty(
+                $property['property'],
+                $property['content'],
+                $property['subProperties'],
+                $property['replace'],
+                $property['type']
+            );
+        }
+
+        $expected = '<meta name="description" content="This is a description" />' . PHP_EOL .
+            '<meta property="og:image" content="/path/to/image2" />' . PHP_EOL .
+            '<meta property="og:image:height" content="200" />' . PHP_EOL .
+            '<meta name="twitter:card" content="This is the Twitter card" />';
+
+        $this->assertEquals($expected, $manager->renderAllProperties());
+    }
+
+    /**
+     * @test
+     */
+    public function checkIfRemovePropertyReallyRemovesProperty()
+    {
+        $manager = new GenericMetaTagManager();
+        $manager->addProperty('description', 'Description');
+        $this->assertEquals([['content' => 'Description', 'subProperties' => []]], $manager->getProperty('description'));
+
+        $manager->removeProperty('description');
+        $this->assertEquals([], $manager->getProperty('description'));
+
+        $manager->addProperty('description', 'Description 1', [], false, 'property');
+        $manager->addProperty('description', 'Description 2', [], false, '');
+        $manager->addProperty('description', 'Description 3', []);
+
+        $this->assertEquals([['content' => 'Description 1', 'subProperties' => []]], $manager->getProperty('description', 'property'));
+
+        $manager->removeProperty('description', 'property');
+        $this->assertEquals([], $manager->getProperty('description', 'property'));
+        $this->assertEquals(
+            [
+                ['content' => 'Description 2', 'subProperties' => []],
+                ['content' => 'Description 3', 'subProperties' => []]
+            ],
+            $manager->getProperty('description')
+        );
+
+        $manager->addProperty('description', 'Title', [], false, 'property');
+        $manager->addProperty('description', 'Title', [], false, 'name');
+        $manager->addProperty('twitter:card', 'Twitter card');
+
+        $manager->removeAllProperties();
+
+        $this->assertEquals([], $manager->getProperty('description'));
+        $this->assertEquals([], $manager->getProperty('description', 'name'));
+        $this->assertEquals([], $manager->getProperty('description', 'property'));
+        $this->assertEquals([], $manager->getProperty('twitter:card'));
+    }
+
+    /**
+     * @return array
+     */
+    public function propertiesProvider()
+    {
+        return [
+            [
+                [
+                    'property' => 'custom-tag',
+                    'content' => 'Test title',
+                    'subProperties' => [],
+                    'replace' => false,
+                    'type' => ''
+                ],
+                [
+                    [
+                        'content' => 'Test title',
+                        'subProperties' => []
+                    ]
+                ],
+                '<meta name="custom-tag" content="Test title" />'
+            ],
+            [
+                [
+                    'property' => 'description',
+                    'content' => 'Custom description',
+                    'subProperties' => [],
+                    'replace' => false,
+                    'type' => ''
+                ],
+                [
+                    [
+                        'content' => 'Custom description',
+                        'subProperties' => []
+                    ]
+                ],
+                '<meta name="description" content="Custom description" />'
+            ],
+            [
+                [
+                    'property' => 'og:image',
+                    'content' => '/path/to/image',
+                    'subProperties' => [],
+                    'replace' => false,
+                    'type' => 'property'
+                ],
+                [
+                    [
+                        'content' => '/path/to/image',
+                        'subProperties' => []
+                    ]
+                ],
+                '<meta property="og:image" content="/path/to/image" />'
+            ],
+            [
+                [
+                    'property' => 'og:image',
+                    'content' => '/path/to/image',
+                    'subProperties' => ['width' => 100],
+                    'replace' => false,
+                    'type' => 'property'
+                ],
+                [
+                    [
+                        'content' => '/path/to/image',
+                        'subProperties' => ['width' => 100]
+                    ]
+                ],
+                '<meta property="og:image" content="/path/to/image" />' . PHP_EOL .
+                    '<meta property="og:image:width" content="100" />'
+            ],
+            [
+                [
+                    'property' => 'og:image:width',
+                    'content' => '100',
+                    'subProperties' => [],
+                    'replace' => false,
+                    'type' => 'property'
+                ],
+                [
+                    [
+                        'content' => '100',
+                        'subProperties' => []
+                    ]
+                ],
+                '<meta property="og:image:width" content="100" />'
+            ]
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/MetaTag/MetaTagManagerRegistryTest.php b/typo3/sysext/core/Tests/Unit/MetaTag/MetaTagManagerRegistryTest.php
new file mode 100644 (file)
index 0000000..d929f94
--- /dev/null
@@ -0,0 +1,234 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\MetaTag\GenericMetaTagManager;
+use TYPO3\CMS\Core\MetaTag\Html5MetaTagManager;
+use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
+use TYPO3\CMS\Core\MetaTag\OpenGraphMetaTagManager;
+use TYPO3\CMS\Core\MetaTag\TwitterCardMetaTagManager;
+
+/**
+ * Test case
+ */
+class MetaTagManagerRegistryTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function checkGetInstanceReturnsMetaTagManagerRegistryInstance()
+    {
+        return $this->assertInstanceOf(MetaTagManagerRegistry::class, MetaTagManagerRegistry::getInstance());
+    }
+
+    /**
+     * @test
+     */
+    public function checkRegisterNonExistingManagerDoesntThrowErrorWhenFetchingManagers()
+    {
+        $metaTagManagerRegistry = MetaTagManagerRegistry::getInstance();
+
+        $metaTagManagerRegistry->registerManager('name', 'fake//class//name');
+        $metaTagManagerRegistry->getAllManagers();
+    }
+
+    /**
+     * @param array $managersToRegister
+     * @param array $expected
+     *
+     * @dataProvider registerMetaTagManagersProvider
+     * @test
+     */
+    public function checkRegisterExistingManagerDoRegister($managersToRegister, $expected)
+    {
+        $metaTagManagerRegistry = new MetaTagManagerRegistry();
+
+        foreach ($managersToRegister as $managerToRegister) {
+            $metaTagManagerRegistry->registerManager(
+                $managerToRegister['name'],
+                $managerToRegister['className'],
+                (array)$managerToRegister['before'],
+                (array)$managerToRegister['after']
+            );
+        }
+
+        // Remove all properties from the manager if it was set by a previous unittest
+        foreach ($metaTagManagerRegistry->getAllManagers() as $manager) {
+            $manager->removeAllProperties();
+        }
+
+        $managers = $metaTagManagerRegistry->getAllManagers();
+
+        $this->assertEquals($expected, $managers);
+    }
+
+    /**
+     * @test
+     */
+    public function checkConditionRaceResultsIntoException()
+    {
+        $input = [
+            'name' => 'opengraph',
+            'className' => OpenGraphMetaTagManager::class,
+            'before' => ['opengraph'],
+            'after' => []
+        ];
+
+        $this->expectException(\UnexpectedValueException::class);
+
+        $metaTagManagerRegistry = new MetaTagManagerRegistry();
+        $metaTagManagerRegistry->registerManager($input['name'], $input['className'], (array)$input['before'], (array)$input['after']);
+        $metaTagManagerRegistry->getAllManagers();
+    }
+    /**
+     * @return array
+     */
+    public function registerMetaTagManagersProvider()
+    {
+        return [
+            [
+                [
+                    [
+                        'name' => 'opengraph',
+                        'className' => OpenGraphMetaTagManager::class,
+                        'before' => [],
+                        'after' => []
+                    ]
+                ],
+                [
+                    new OpenGraphMetaTagManager(),
+                    new GenericMetaTagManager()
+                ]
+            ],
+            [
+                [
+                    [
+                        'name' => 'opengraph',
+                        'className' => OpenGraphMetaTagManager::class,
+                        'before' => [],
+                        'after' => []
+                    ],
+                    [
+                        'name' => 'opengraph',
+                        'className' => OpenGraphMetaTagManager::class,
+                        'before' => [],
+                        'after' => []
+                    ],
+                ],
+                [
+                    new OpenGraphMetaTagManager(),
+                    new GenericMetaTagManager()
+                ]
+            ],
+            [
+                [
+                    [
+                        'name' => 'opengraph',
+                        'className' => OpenGraphMetaTagManager::class,
+                        'before' => [],
+                        'after' => []
+                    ],
+                    [
+                        'name' => 'html5',
+                        'className' => Html5MetaTagManager::class,
+                        'before' => [],
+                        'after' => []
+                    ],
+                ],
+                [
+                    new Html5MetaTagManager(),
+                    new OpenGraphMetaTagManager(),
+                    new GenericMetaTagManager()
+                ]
+            ],
+            [
+                [
+                    [
+                        'name' => 'opengraph',
+                        'className' => OpenGraphMetaTagManager::class,
+                        'before' => ['html5'],
+                        'after' => []
+                    ],
+                    [
+                        'name' => 'html5',
+                        'className' => Html5MetaTagManager::class,
+                        'before' => [],
+                        'after' => []
+                    ],
+                ],
+                [
+                    new OpenGraphMetaTagManager(),
+                    new Html5MetaTagManager(),
+                    new GenericMetaTagManager()
+                ]
+            ],
+            [
+                [
+                    [
+                        'name' => 'opengraph',
+                        'className' => OpenGraphMetaTagManager::class,
+                        'before' => [],
+                        'after' => []
+                    ],
+                    [
+                        'name' => 'html5',
+                        'className' => Html5MetaTagManager::class,
+                        'before' => [],
+                        'after' => ['opengraph']
+                    ],
+                ],
+                [
+                    new OpenGraphMetaTagManager(),
+                    new Html5MetaTagManager(),
+                    new GenericMetaTagManager()
+                ]
+            ],
+            [
+                [
+                    [
+                        'name' => 'opengraph',
+                        'className' => OpenGraphMetaTagManager::class,
+                        'before' => [],
+                        'after' => []
+                    ],
+                    [
+                        'name' => 'html5',
+                        'className' => Html5MetaTagManager::class,
+                        'before' => [],
+                        'after' => ['twitter']
+                    ],
+                    [
+                        'name' => 'twitter',
+                        'className' => TwitterCardMetaTagManager::class,
+                        'before' => [],
+                        'after' => ['opengraph']
+                    ],
+                ],
+                [
+                    new OpenGraphMetaTagManager(),
+                    new TwitterCardMetaTagManager(),
+                    new Html5MetaTagManager(),
+                    new GenericMetaTagManager()
+                ]
+            ],
+            [
+                [],
+                [
+                    new GenericMetaTagManager()
+                ]
+            ],
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/MetaTag/OpenGraphMetaTagManagerTest.php b/typo3/sysext/core/Tests/Unit/MetaTag/OpenGraphMetaTagManagerTest.php
new file mode 100644 (file)
index 0000000..e062a5f
--- /dev/null
@@ -0,0 +1,205 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\MetaTag;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\MetaTag\OpenGraphMetaTagManager;
+
+/**
+ * Test case
+ */
+class OpenGraphMetaTagManagerTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function checkIfGetAllHandledPropertiesReturnsNonEmptyArray()
+    {
+        $manager = new OpenGraphMetaTagManager();
+        $handledProperties = $manager->getAllHandledProperties();
+
+        $this->assertNotEmpty($handledProperties);
+    }
+
+    /**
+     * @dataProvider propertiesProvider
+     *
+     * @test
+     */
+    public function checkIfPropertyIsStoredAfterAddingProperty($property, $expected, $expectedRenderedTag)
+    {
+        $manager = new OpenGraphMetaTagManager();
+        $manager->addProperty(
+            $property['property'],
+            $property['content'],
+            (array)$property['subProperties']
+        );
+
+        $this->assertEquals($expected, $manager->getProperty($property['property']));
+        $this->assertEquals($expectedRenderedTag, $manager->renderProperty($property['property']));
+    }
+
+    /**
+     * @test
+     */
+    public function checkIfAddingOnlySubPropertyAndNoMainPropertyIsReturningException()
+    {
+        $manager = new OpenGraphMetaTagManager();
+
+        $this->expectException(\UnexpectedValueException::class);
+        $manager->addProperty('og:image:width', '400');
+    }
+
+    /**
+     * @test
+     */
+    public function checkRenderAllPropertiesRendersCorrectMetaTags()
+    {
+        $properties = [
+            [
+                'property' => 'og:title',
+                'content' => 'This is a title',
+                'subProperties' => [],
+                'replace' => false,
+                'type' => ''
+            ],
+            [
+                'property' => 'og:image',
+                'content' => '/path/to/image',
+                'subProperties' => [
+                    'width' => 400
+                ],
+                'replace' => false,
+                'type' => ''
+            ],
+            [
+                'property' => 'og:image:height',
+                'content' => '200',
+                'subProperties' => [],
+                'replace' => false,
+                'type' => ''
+            ],
+            [
+                'property' => 'og:title',
+                'content' => 'This is the new title',
+                'subProperties' => [],
+                'replace' => true,
+                'type' => ''
+            ],
+            [
+                'property' => 'og:image',
+                'content' => '/path/to/image2',
+                'subProperties' => [],
+                'replace' => false,
+                'type' => ''
+            ],
+        ];
+
+        $manager = new OpenGraphMetaTagManager();
+        foreach ($properties as $property) {
+            $manager->addProperty(
+                $property['property'],
+                $property['content'],
+                $property['subProperties'],
+                $property['replace'],
+                $property['type']
+            );
+        }
+
+        $expected = '<meta property="og:image" content="/path/to/image" />' . PHP_EOL .
+            '<meta property="og:image:width" content="400" />' . PHP_EOL .
+            '<meta property="og:image:height" content="200" />' . PHP_EOL .
+            '<meta property="og:image" content="/path/to/image2" />' . PHP_EOL .
+            '<meta property="og:title" content="This is the new title" />';
+
+        $this->assertEquals($expected, $manager->renderAllProperties());
+    }
+
+    /**
+     * @test
+     */
+    public function checkIfRemovePropertyReallyRemovesProperty()
+    {
+        $manager = new OpenGraphMetaTagManager();
+        $manager->addProperty('og:title', 'Title');
+        $this->assertEquals([['content' => 'Title', 'subProperties' => []]], $manager->getProperty('og:title'));
+
+        $manager->removeProperty('og:title');
+        $this->assertEquals([], $manager->getProperty('og:title'));
+
+        $manager->addProperty('og:title', 'Title');
+        $manager->addProperty('og:description', 'Description');
+
+        $manager->removeAllProperties();
+
+        $this->assertEquals([], $manager->getProperty('og:title'));
+        $this->assertEquals([], $manager->getProperty('og:description'));
+    }
+
+    /**
+     * @return array
+     */
+    public function propertiesProvider()
+    {
+        return [
+            [
+                [
+                    'property' => 'og:title',
+                    'content' => 'Test title',
+                    'subProperties' => []
+                ],
+                [
+                    [
+                        'content' => 'Test title',
+                        'subProperties' => []
+                    ]
+                ],
+                '<meta property="og:title" content="Test title" />'
+            ],
+            [
+                [
+                    'property' => 'og:image',
+                    'content' => '/path/to/image',
+                    'subProperties' => []
+                ],
+                [
+                    [
+                        'content' => '/path/to/image',
+                        'subProperties' => []
+                    ]
+                ],
+                '<meta property="og:image" content="/path/to/image" />'
+            ],
+            [
+                [
+                    'property' => 'og:image',
+                    'content' => '/path/to/image',
+                    'subProperties' => ['width' => [400], 'height' => [400]]
+                ],
+                [
+                    [
+                        'content' => '/path/to/image',
+                        'subProperties' => [
+                            'width' => [400],
+                            'height' => [400]
+                        ]
+                    ]
+                ],
+                '<meta property="og:image" content="/path/to/image" />' . PHP_EOL .
+                '<meta property="og:image:width" content="400" />' . PHP_EOL .
+                '<meta property="og:image:height" content="400" />'
+            ]
+        ];
+    }
+}
index 74c2081..c0bcae5 100644 (file)
@@ -136,3 +136,22 @@ unset($extractorRegistry);
 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(
     'TCEMAIN.translateToMessage = Translate to %s:'
 );
+
+$metaTagManagerRegistry = \TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry::getInstance();
+$metaTagManagerRegistry->registerManager(
+    'opengraph',
+    \TYPO3\CMS\Core\MetaTag\OpenGraphMetaTagManager::class
+);
+$metaTagManagerRegistry->registerManager(
+    'html5',
+    \TYPO3\CMS\Core\MetaTag\Html5MetaTagManager::class
+);
+$metaTagManagerRegistry->registerManager(
+    'edge',
+    \TYPO3\CMS\Core\MetaTag\EdgeMetaTagManager::class
+);
+$metaTagManagerRegistry->registerManager(
+    'twitter',
+    \TYPO3\CMS\Core\MetaTag\TwitterCardMetaTagManager::class
+);
+unset($metaTagManagerRegistry);
index 1f6d554..37b66e9 100644 (file)
@@ -877,11 +877,13 @@ class PageGenerator
         $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
         $conf = $typoScriptService->convertTypoScriptArrayToPlainArray($metaTagTypoScript);
         foreach ($conf as $key => $properties) {
+            $replace = false;
             if (is_array($properties)) {
                 $nodeValue = $properties['_typoScriptNodeValue'] ?? '';
                 $value = trim($cObj->stdWrap($nodeValue, $metaTagTypoScript[$key . '.']));
                 if ($value === '' && !empty($properties['value'])) {
                     $value = $properties['value'];
+                    $replace = false;
                 }
             } else {
                 $value = $properties;
@@ -894,13 +896,16 @@ class PageGenerator
             if (is_array($properties) && !empty($properties['attribute'])) {
                 $attribute = $properties['attribute'];
             }
+            if (is_array($properties) && !empty($properties['replace'])) {
+                $replace = true;
+            }
 
             if (!is_array($value)) {
                 $value = (array)$value;
             }
             foreach ($value as $subValue) {
                 if (trim($subValue) !== '') {
-                    $pageRenderer->setMetaTag($attribute, $key, $subValue);
+                    $pageRenderer->setMetaTag($attribute, $key, $subValue, [], $replace);
                 }
             }
         }
index 9502b1c..80c850e 100644 (file)
@@ -237,7 +237,7 @@ class PageGeneratorTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
 
         PageGenerator::renderContentWithHeader('');
 
-        $pageRendererProphecy->setMetaTag($expectedTags['type'], $expectedTags['name'], $expectedTags['content'])->shouldHaveBeenCalled();
+        $pageRendererProphecy->setMetaTag($expectedTags['type'], $expectedTags['name'], $expectedTags['content'], [], false)->shouldHaveBeenCalled();
     }
 
     /**
@@ -365,7 +365,7 @@ class PageGeneratorTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
 
         PageGenerator::renderContentWithHeader('');
 
-        $pageRendererProphecy->setMetaTag($expectedTags[0]['type'], $expectedTags[0]['name'], $expectedTags[0]['content'])->shouldHaveBeenCalled();
-        $pageRendererProphecy->setMetaTag($expectedTags[1]['type'], $expectedTags[1]['name'], $expectedTags[1]['content'])->shouldHaveBeenCalled();
+        $pageRendererProphecy->setMetaTag($expectedTags[0]['type'], $expectedTags[0]['name'], $expectedTags[0]['content'], [], false)->shouldHaveBeenCalled();
+        $pageRendererProphecy->setMetaTag($expectedTags[1]['type'], $expectedTags[1]['name'], $expectedTags[1]['content'], [], false)->shouldHaveBeenCalled();
     }
 }