[TASK] Add update wizard to migrate <link> tags to <a> tags 76/51276/17
authorChristian Kuhn <lolli@schwarzbu.ch>
Wed, 11 Jan 2017 22:42:51 +0000 (23:42 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Mon, 3 Apr 2017 10:45:34 +0000 (12:45 +0200)
Since the RteHtmlParser now stores updated content as <a>
tags instead of <link> tags an update wizard is provided
to convert links in all records that have input fields, textarea fields
or flexforms.

Resolves: #79305
Releases: master
Change-Id: I3f52445d7fd82a999f3cff236b37649c77449d5c
Reviewed-on: https://review.typo3.org/51276
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
Reviewed-by: Jigal van Hemert <jigal.van.hemert@typo3.org>
Tested-by: Jigal van Hemert <jigal.van.hemert@typo3.org>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/core/Classes/Html/RteHtmlParser.php
typo3/sysext/core/Classes/LinkHandling/Exception/UnknownLinkHandlerException.php [new file with mode: 0644]
typo3/sysext/core/Classes/LinkHandling/Exception/UnknownUrnException.php [new file with mode: 0644]
typo3/sysext/core/Classes/LinkHandling/LinkService.php
typo3/sysext/core/Tests/Unit/LinkHandling/LinkServiceTest.php
typo3/sysext/install/Classes/Updates/DatabaseRowsUpdateWizard.php
typo3/sysext/install/Classes/Updates/RowUpdater/RteLinkSyntaxUpdater.php [new file with mode: 0644]

index 90e3bff..38dea74 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Core\Html;
  */
 
 use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Resource;
@@ -599,7 +600,11 @@ class RteHtmlParser extends HtmlParser
                 $linkService = GeneralUtility::makeInstance(LinkService::class);
                 $linkInformation = $linkService->resolve($tagCode['url']);
 
-                $href = $linkService->asString($linkInformation);
+                try {
+                    $href = $linkService->asString($linkInformation);
+                } catch (UnknownLinkHandlerException $e) {
+                    $href = '';
+                }
 
                 // Modify parameters by a hook
                 if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_parsehtml_proc.php']['modifyParams_LinksRte_PostProc']) && is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_parsehtml_proc.php']['modifyParams_LinksRte_PostProc'])) {
diff --git a/typo3/sysext/core/Classes/LinkHandling/Exception/UnknownLinkHandlerException.php b/typo3/sysext/core/Classes/LinkHandling/Exception/UnknownLinkHandlerException.php
new file mode 100644 (file)
index 0000000..de9f701
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+namespace TYPO3\CMS\Core\LinkHandling\Exception;
+
+/*
+ * 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\Exception;
+
+/**
+ * Exception raised if no matching link handler is found.
+ */
+class UnknownLinkHandlerException extends Exception
+{
+}
diff --git a/typo3/sysext/core/Classes/LinkHandling/Exception/UnknownUrnException.php b/typo3/sysext/core/Classes/LinkHandling/Exception/UnknownUrnException.php
new file mode 100644 (file)
index 0000000..ad5d2f5
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+namespace TYPO3\CMS\Core\LinkHandling\Exception;
+
+/*
+ * 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\Exception;
+
+/**
+ * Exception raised if urn is not known.
+ */
+class UnknownUrnException extends Exception
+{
+}
index 29ec07b..8b6ef7f 100644 (file)
@@ -78,7 +78,7 @@ class LinkService implements SingletonInterface
         try {
             // Check if the new syntax with "t3://" is used
             return $this->resolveByStringRepresentation($linkParameter);
-        } catch (\InvalidArgumentException $e) {
+        } catch (Exception\UnknownUrnException $e) {
             $legacyLinkNotationConverter = GeneralUtility::makeInstance(LegacyLinkNotationConverter::class);
             return $legacyLinkNotationConverter->resolve($linkParameter);
         }
@@ -89,6 +89,8 @@ class LinkService implements SingletonInterface
      *
      * @param string $urn
      * @return array
+     * @throws Exception\UnknownLinkHandlerException
+     * @throws Exception\UnknownUrnException
      */
     public function resolveByStringRepresentation(string $urn): array
     {
@@ -108,7 +110,7 @@ class LinkService implements SingletonInterface
                 $result = $this->handlers[$type]->resolveHandlerData($data);
                 $result['type'] = $type;
             } else {
-                throw new \InvalidArgumentException('LinkHandler for ' . $type . ' was not registered', 1460581769);
+                throw new Exception\UnknownLinkHandlerException('LinkHandler for ' . $type . ' was not registered', 1460581769);
             }
             // this was historically named "section"
             if ($fragment) {
@@ -121,7 +123,7 @@ class LinkService implements SingletonInterface
             $result = $this->handlers[self::TYPE_EMAIL]->resolveHandlerData(['email' => $urn]);
             $result['type'] = self::TYPE_EMAIL;
         } else {
-            throw new \InvalidArgumentException('No valid URN to resolve found', 1457177667);
+            throw new Exception\UnknownUrnException('No valid URN to resolve found', 1457177667);
         }
 
         return $result;
@@ -138,13 +140,13 @@ class LinkService implements SingletonInterface
      *
      * @param array $parameters
      * @return string
-     * @throws \InvalidArgumentException
+     * @throws Exception\UnknownLinkHandlerException
      */
     public function asString(array $parameters): string
     {
         if (is_object($this->handlers[$parameters['type']])) {
             return $this->handlers[$parameters['type']]->asString($parameters);
         }
-        throw new \InvalidArgumentException('No valid handlers found for type: ' . $parameters['type'], 1460629247);
+        throw new Exception\UnknownLinkHandlerException('No valid handlers found for type: ' . $parameters['type'], 1460629247);
     }
 }
index 1b67e8b..94abe0b 100644 (file)
@@ -15,8 +15,9 @@ namespace TYPO3\CMS\Core\Tests\Unit\LinkHandling;
  */
 
 use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
-class LinkServiceTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+class LinkServiceTest extends UnitTestCase
 {
     /**
      * Data to resolve strings to arrays and vice versa, external, mail, page
index 0cc2df8..3c9d36e 100644 (file)
@@ -14,12 +14,14 @@ namespace TYPO3\CMS\Install\Updates;
  *
  * The TYPO3 project - inspiring people to share!
  */
+
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Registry;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Updates\RowUpdater\ImageCropUpdater;
 use TYPO3\CMS\Install\Updates\RowUpdater\L10nModeUpdater;
 use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
+use TYPO3\CMS\Install\Updates\RowUpdater\RteLinkSyntaxUpdater;
 
 /**
  * This is a generic updater to migrate content of TCA rows.
@@ -51,6 +53,7 @@ class DatabaseRowsUpdateWizard extends AbstractUpdate
     protected $rowUpdater = [
         L10nModeUpdater::class,
         ImageCropUpdater::class,
+        RteLinkSyntaxUpdater::class,
     ];
 
     /**
diff --git a/typo3/sysext/install/Classes/Updates/RowUpdater/RteLinkSyntaxUpdater.php b/typo3/sysext/install/Classes/Updates/RowUpdater/RteLinkSyntaxUpdater.php
new file mode 100644 (file)
index 0000000..6b1dab3
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Install\Updates\RowUpdater;
+
+/*
+ * 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\LinkHandling\Exception\UnknownLinkHandlerException;
+use TYPO3\CMS\Core\LinkHandling\Exception\UnknownUrnException;
+use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
+
+/**
+ * Move '<link ...' syntax to '<a href' in rte fields
+ */
+class RteLinkSyntaxUpdater implements RowUpdaterInterface
+{
+    /**
+     * Table list with field list that may have links them
+     *
+     * @var array
+     */
+    protected $tableFieldListToConsider = [];
+
+    /**
+     * @var array Table names that should be ignored.
+     */
+    protected $blackListedTables = [
+        'sys_log',
+        'sys_history',
+    ];
+
+    /**
+     * Regular expressions to match the <link ...>content</link> inside
+     * @var array
+     */
+    protected $regularExpressions = [
+        'default' => '#(?\'tag\'<link\\s+(?\'typolink\'[^>]+)>)(?\'content\'(?:(?!</link>).)*)</link>#msi',
+        'flex' => '#(?\'tag\'&lt;link\\s+(?\'typolink\'.+?)&gt;)(?\'content\'(?:(?!&lt;/link&gt;).)*)&lt;/link&gt;#msi'
+    ];
+
+    /**
+     * Get title
+     *
+     * @return string Title
+     */
+    public function getTitle(): string
+    {
+        return 'Scan for old "<link>" syntax in richtext and text fields and update to "<a href>"';
+    }
+
+    /**
+     * Return true if a table may have RTE fields
+     *
+     * @param string $tableName Table name to check
+     * @return bool True if this table potentially has RTE fields
+     */
+    public function hasPotentialUpdateForTable(string $tableName): bool
+    {
+        if (!is_array($GLOBALS['TCA'][$tableName])) {
+            throw new \RuntimeException(
+                'Globals TCA of ' . $tableName . ' must be an array',
+                1484173035
+            );
+        }
+        $result = false;
+        if (in_array($tableName, $this->blackListedTables, true)) {
+            return $result;
+        }
+        $tcaOfTable = $GLOBALS['TCA'][$tableName];
+        if (!is_array($tcaOfTable['columns'])) {
+            return $result;
+        }
+        foreach ($tcaOfTable['columns'] as $fieldName => $fieldConfiguration) {
+            if (isset($fieldConfiguration['config']['type'])
+                && in_array($fieldConfiguration['config']['type'], ['input', 'text', 'flex'], true)
+            ) {
+                $result = true;
+                if (!is_array($this->tableFieldListToConsider[$tableName])) {
+                    $this->tableFieldListToConsider[$tableName] = [];
+                }
+                $this->tableFieldListToConsider[$tableName][] = $fieldName;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Update "<link" tags in RTE fields
+     *
+     * @param string $tableName Table name
+     * @param array $row Given row data
+     * @return array Modified row data
+     */
+    public function updateTableRow(string $tableName, array $row): array
+    {
+        if (!is_array($this->tableFieldListToConsider)) {
+            throw new \RuntimeException(
+                'Parent should not call me with a table name I do not consider relevant for update',
+                1484173650
+            );
+        }
+        $fieldsToScan = $this->tableFieldListToConsider[$tableName];
+        foreach ($fieldsToScan as $fieldName) {
+            $row[$fieldName] = $this->transformLinkTagsIfFound(
+                $row[$fieldName],
+                $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'] === 'flex'
+            );
+        }
+        return $row;
+    }
+
+    /**
+     * Finds all <link> tags and calls the typolink codec service and the link service (twice) to get a string
+     * representation of the href part, and then builds an anchor tag.
+     *
+     * @param mixed $content The content to process
+     * @param bool $isFlexformField If true the content is htmlspecialchar()'d and must be treated as such
+     * @return mixed the modified content
+     */
+    protected function transformLinkTagsIfFound($content, $isFlexformField)
+    {
+        if (is_string($content)
+            && !empty($content)
+            && (stripos($content, '<link') !== false || stripos($content, '&lt;link') !== false)
+        ) {
+            $content = preg_replace_callback(
+                $this->regularExpressions[$isFlexformField ? 'flex' : 'default'],
+                function ($matches) use ($isFlexformField) {
+                    $typoLink = $isFlexformField ? htmlspecialchars_decode($matches['typolink']) : $matches['typolink'];
+                    $typoLinkParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($typoLink);
+                    $anchorTagAttributes = [
+                        'target' => $typoLinkParts['target'],
+                        'class' => $typoLinkParts['class'],
+                        'title' => $typoLinkParts['title'],
+                    ];
+
+                    $link = $typoLinkParts['url'];
+                    if (!empty($typoLinkParts['additionalParams'])) {
+                        $link .= (strpos($link, '?') === false ? '?' : '&') . ltrim($typoLinkParts['additionalParams'], '&');
+                    }
+
+                    try {
+                        $linkService = GeneralUtility::makeInstance(LinkService::class);
+                        // Ensure the old syntax is converted to the new t3:// syntax, if necessary
+                        $linkParts = $linkService->resolve($link);
+                        $anchorTagAttributes['href'] = $linkService->asString($linkParts);
+                        $newLink = '<a ' . GeneralUtility::implodeAttributes($anchorTagAttributes, true) . '>' .
+                            ($isFlexformField ? htmlspecialchars_decode($matches['content']) : $matches['content']) .
+                            '</a>';
+                        if ($isFlexformField) {
+                            $newLink = htmlspecialchars($newLink);
+                        }
+                    } catch (UnknownLinkHandlerException $e) {
+                        $newLink = $matches[0];
+                    } catch (UnknownUrnException $e) {
+                        $newLink = $matches[0];
+                    }
+
+                    return $newLink;
+                },
+                $content
+            );
+        }
+        return $content;
+    }
+}