Add rel="ugc" for links within user generated content
authorMarcel Werk <burntime@woltlab.com>
Thu, 6 Aug 2020 21:19:27 +0000 (23:19 +0200)
committerMarcel Werk <burntime@woltlab.com>
Thu, 6 Aug 2020 21:19:27 +0000 (23:19 +0200)
16 files changed:
com.woltlab.wcf/templates/quoteMetaCode.tpl
com.woltlab.wcf/templates/userInformationButtons.tpl
wcfsetup/install/files/lib/data/article/content/ArticleContent.class.php
wcfsetup/install/files/lib/data/box/content/BoxContent.class.php
wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php
wcfsetup/install/files/lib/data/page/content/PageContent.class.php
wcfsetup/install/files/lib/system/bbcode/MediaBBCode.class.php
wcfsetup/install/files/lib/system/bbcode/SimpleMessageParser.class.php
wcfsetup/install/files/lib/system/html/output/HtmlOutputProcessor.class.php
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeA.class.php
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php
wcfsetup/install/files/lib/system/option/user/FacebookUserOptionOutput.class.php
wcfsetup/install/files/lib/system/option/user/TwitterUserOptionOutput.class.php
wcfsetup/install/files/lib/system/option/user/URLUserOptionOutput.class.php
wcfsetup/install/files/lib/system/template/plugin/AnchorAttributesFunctionTemplatePlugin.class.php
wcfsetup/install/files/lib/util/StringUtil.class.php

index ebbfe33476b64739fec0db18465e701aae7f7d70..b9a0fc49e0864b9c87d9d8ccc09fc30a22990c83 100644 (file)
@@ -11,7 +11,7 @@
                <span class="quoteBoxTitle">
                        {if $quoteAuthor}
                                {if $quoteLink}
-                                       <a {anchorAttributes url=$quoteLink}>{lang}wcf.bbcode.quote.title{/lang}</a>
+                                       <a {anchorAttributes url=$quoteLink isUgc=true}>{lang}wcf.bbcode.quote.title{/lang}</a>
                                {else}
                                        {lang}wcf.bbcode.quote.title{/lang}
                                {/if}
index bd9ed1fcb2fd6c1f97a9f937905a6251b7933392..0a5c4093223de43ed746195d874351aeae6e2628 100644 (file)
@@ -3,7 +3,7 @@
                <ul class="buttonList iconList">
                        {content}
                                {if $user->homepage && $user->homepage != 'http://'}
-                                       <li><a class="jsTooltip" title="{lang}wcf.user.option.homepage{/lang}" {anchorAttributes url=$user->homepage appendClassname=false}><span class="icon icon16 fa-home"></span> <span class="invisible">{lang}wcf.user.option.homepage{/lang}</span></a></li>
+                                       <li><a class="jsTooltip" title="{lang}wcf.user.option.homepage{/lang}" {anchorAttributes url=$user->homepage appendClassname=false  isUgc=true}><span class="icon icon16 fa-home"></span> <span class="invisible">{lang}wcf.user.option.homepage{/lang}</span></a></li>
                                {/if}
                                
                                {if $user->userID != $__wcf->user->userID}
index b7043678cb9bc6fe9d1cdc6d13d3b708fd7625d8..f5216fdcbf8b1954981688fd317dae1812f91790 100644 (file)
@@ -81,6 +81,7 @@ class ArticleContent extends DatabaseObject implements ILinkableObject, IRouteCo
                else {
                        $htmlOutputProcessor = new HtmlOutputProcessor();
                        $htmlOutputProcessor->setOutputType('text/plain');
+                       $htmlOutputProcessor->enableUgc(false);
                        $htmlOutputProcessor->process($this->content, 'com.woltlab.wcf.article.content', $this->articleContentID, false, $this->languageID);
                        
                        return nl2br(StringUtil::encodeHTML(StringUtil::truncate($htmlOutputProcessor->getHtml(), 500)), false);
@@ -94,6 +95,7 @@ class ArticleContent extends DatabaseObject implements ILinkableObject, IRouteCo
         */
        public function getFormattedContent() {
                $processor = new HtmlOutputProcessor();
+               $processor->enableUgc(false);
                $processor->process($this->content, 'com.woltlab.wcf.article.content', $this->articleContentID, false, $this->languageID);
                
                return $processor->getHtml();
@@ -106,6 +108,7 @@ class ArticleContent extends DatabaseObject implements ILinkableObject, IRouteCo
         */
        public function getAmpFormattedContent() {
                $processor = new AmpHtmlOutputProcessor();
+               $processor->enableUgc(false);
                $processor->process($this->content, 'com.woltlab.wcf.article.content', $this->articleContentID);
                
                return $processor->getHtml();
@@ -149,6 +152,7 @@ class ArticleContent extends DatabaseObject implements ILinkableObject, IRouteCo
                        case 'text/plain':
                                $processor = new HtmlOutputProcessor();
                                $processor->setOutputType('text/plain');
+                               $processor->enableUgc(false);
                                $processor->process($this->content, 'com.woltlab.wcf.article.content', $this->articleContentID);
                                
                                return $processor->getHtml();
@@ -156,6 +160,7 @@ class ArticleContent extends DatabaseObject implements ILinkableObject, IRouteCo
                                // parse and return message
                                $processor = new HtmlOutputProcessor();
                                $processor->setOutputType('text/simplified-html');
+                               $processor->enableUgc(false);
                                $processor->process($this->content, 'com.woltlab.wcf.article.content', $this->articleContentID);
                                
                                return $processor->getHtml();
index c09ae8ec2a56ed790aadfb6f165a87074f12b3d8..d8ab70f7d7bcb7337ec9af1fb97017ca19b058ef 100644 (file)
@@ -103,6 +103,7 @@ class BoxContent extends DatabaseObject {
         */
        public function getFormattedContent() {
                $processor = new HtmlOutputProcessor();
+               $processor->enableUgc(false);
                $processor->process($this->content, 'com.woltlab.wcf.box.content', $this->boxContentID);
                
                return $processor->getHtml();
index c55cde48b35b7bcb862ce894fab74d8b2e8a9d27..168e60950ebc51217d44c0442cf375c021a8dbea 100644 (file)
@@ -177,7 +177,7 @@ abstract class CustomOption extends Option implements ITitledObject {
                        
                        /** @noinspection PhpMissingBreakStatementInspection */
                        case 'URL':
-                               if (!$forcePlaintext) return StringUtil::getAnchorTag($this->optionValue);
+                               if (!$forcePlaintext) return StringUtil::getAnchorTag($this->optionValue, '', true, true);
                                // fallthrough
                                
                        default:
index 4434f93e48c000eacabeb92416a16e6c8287709f..dbb4e95b0ecc6eaefd36e70516f0e74a6266ac23 100644 (file)
@@ -47,6 +47,7 @@ class PageContent extends DatabaseObject implements ILinkableObject {
                MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.page.content', [$this->pageContentID]);
                
                $processor = new HtmlOutputProcessor();
+               $processor->enableUgc(false);
                $processor->process($this->content, 'com.woltlab.wcf.page.content', $this->pageContentID);
                
                return $processor->getHtml();
index 49fca2711bb6b9764324dcb66f6eb1f8b092a86d..f0ecae44729b015815e992bfa958c375cbd61d15 100644 (file)
@@ -23,7 +23,7 @@ class MediaBBCode extends AbstractBBCode {
                        foreach (BBCodeMediaProvider::getCache() as $provider) {
                                if ($provider->matches($content)) {
                                        if ($parser instanceof HtmlBBCodeParser && $parser->getIsGoogleAmp()) {
-                                               return StringUtil::getAnchorTag($content);
+                                               return StringUtil::getAnchorTag($content, '', true, true);
                                        }
                                        else {
                                                return $provider->getOutput($content);
@@ -34,7 +34,7 @@ class MediaBBCode extends AbstractBBCode {
                else if ($parser->getOutputType() == 'text/simplified-html' && !$parser->getRemoveLinks()) {
                        foreach (BBCodeMediaProvider::getCache() as $provider) {
                                if ($provider->matches($content)) {
-                                       return StringUtil::getAnchorTag($content);
+                                       return StringUtil::getAnchorTag($content, '', true, true);
                                }
                        }
                }
index ff1b66526a4e72c38171ae8f3d0bd14751eb5ace..f9150502b24fa2e4df812960c8cd0eff632f1e9a 100644 (file)
@@ -201,7 +201,7 @@ class SimpleMessageParser extends SingletonFactory {
                                $url = 'http://'.$url;
                        }
                        
-                       $text = str_replace($hash, StringUtil::getAnchorTag($url), $text);
+                       $text = str_replace($hash, StringUtil::getAnchorTag($url, '', true, true), $text);
                }
                
                foreach ($this->cachedEmails as $hash => $email) {
index ea9ec8400b9187a2a855208df30739306e0baf64..e414efc56698b7b153d140b2663104344305045e 100644 (file)
@@ -47,6 +47,12 @@ class HtmlOutputProcessor extends AbstractHtmlProcessor {
         */
        protected $outputType = 'text/html';
        
+       /**
+        * enables rel=ugc for external links
+        * @var bool
+        */
+       protected $ugc = true;
+       
        /**
         * Processes the input html string.
         *
@@ -113,4 +119,22 @@ class HtmlOutputProcessor extends AbstractHtmlProcessor {
                
                return $this->htmlOutputNodeProcessor;
        }
+       
+       /**
+        * Enables rel=ugc for external links.
+        * 
+        * @param bool $enable
+        */
+       public function enableUgc($enable = true) {
+               $this->ugc = $enable;
+       }
+       
+       /**
+        * Returns true, if content is user-generated.
+        * 
+        * @return bool
+        */
+       public function isUgc() {
+               return $this->ugc;
+       }
 }
index 2c46cd12877a61ed7569e92ad3031f9f03076957..9ec37e7af682eda98fb23b321fd78dcf90a0f976 100644 (file)
@@ -32,7 +32,8 @@ class HtmlOutputNodeA extends AbstractHtmlOutputNode {
                                $element->setAttribute('href', preg_replace('~^https?://~', RouteHandler::getProtocol(), $href));
                        }
                        else {
-                               self::markLinkAsExternal($element);
+                               /** @var HtmlOutputNodeProcessor $htmlNodeProcessor */
+                               self::markLinkAsExternal($element, $htmlNodeProcessor->getHtmlProcessor()->isUgc());
                        }
                        
                        $value = StringUtil::trim($element->textContent);
@@ -67,8 +68,9 @@ class HtmlOutputNodeA extends AbstractHtmlOutputNode {
         * Marks an element as external.
         * 
         * @param       \DOMElement     $element
+        * @param       bool            $isUgc
         */
-       public static function markLinkAsExternal(\DOMElement $element) {
+       public static function markLinkAsExternal(\DOMElement $element, $isUgc = false) {
                $element->setAttribute('class', 'externalURL');
                
                $rel = 'nofollow';
@@ -77,6 +79,10 @@ class HtmlOutputNodeA extends AbstractHtmlOutputNode {
                        
                        $element->setAttribute('target', '_blank');
                }
+               if ($isUgc) {
+                       $rel .= ' ugc';
+               }
+               
                $element->setAttribute('rel', $rel);
                
                // If the link contains only a single image that is floated to the right,
index 6f4e0b90505165ebdc60d26cc830d794f8b056f2..0b9fa6ca41076904713e33ee1ac87c544084df48 100644 (file)
@@ -85,7 +85,8 @@ class HtmlOutputNodeImg extends AbstractHtmlOutputNode {
                                        if (IMAGE_PROXY_INSECURE_ONLY && $urlComponents['scheme'] === 'https') {
                                                // proxy is enabled for insecure connections only
                                                if (!IMAGE_ALLOW_EXTERNAL_SOURCE && !$this->isAllowedOrigin($src)) {
-                                                       $this->replaceExternalSource($element, $src);
+                                                       /** @var HtmlOutputNodeProcessor $htmlNodeProcessor */
+                                                       $this->replaceExternalSource($element, $src, $htmlNodeProcessor->getHtmlProcessor()->isUgc());
                                                }
                                                
                                                continue;
@@ -133,7 +134,8 @@ class HtmlOutputNodeImg extends AbstractHtmlOutputNode {
                                        }
                                }
                                else if (!IMAGE_ALLOW_EXTERNAL_SOURCE && !$this->isAllowedOrigin($src)) {
-                                       $this->replaceExternalSource($element, $src);
+                                       /** @var HtmlOutputNodeProcessor $htmlNodeProcessor */
+                                       $this->replaceExternalSource($element, $src, $htmlNodeProcessor->getHtmlProcessor()->isUgc());
                                }
                                else if (MESSAGE_FORCE_SECURE_IMAGES && Url::parse($src)['scheme'] === 'http') {
                                        // rewrite protocol to `https`
@@ -148,15 +150,16 @@ class HtmlOutputNodeImg extends AbstractHtmlOutputNode {
         * 
         * @param       \DOMElement     $element
         * @param       string          $src
+        * @param       bool            $isUgc
         */
-       protected function replaceExternalSource(\DOMElement $element, $src) {
+       protected function replaceExternalSource(\DOMElement $element, $src, $isUgc = false) {
                $element->parentNode->insertBefore($element->ownerDocument->createTextNode('['.WCF::getLanguage()->get('wcf.bbcode.image.blocked').': '), $element);
                
                if (!DOMUtil::hasParent($element, 'a')) {
                        $link = $element->ownerDocument->createElement('a');
                        $link->setAttribute('href', $src);
                        $link->textContent = $src;
-                       HtmlOutputNodeA::markLinkAsExternal($link);
+                       HtmlOutputNodeA::markLinkAsExternal($link, $isUgc);
                }
                else {
                        $link = $element->ownerDocument->createTextNode($src);
index 741eca8f00d1b75ef7880761064e69f986b1987a..14a4c053c3a821602970562cab2a561ea3e354e0 100644 (file)
@@ -19,6 +19,6 @@ class FacebookUserOptionOutput implements IUserOptionOutput {
        public function getOutput(User $user, UserOption $option, $value) {
                if (empty($value)) return '';
                
-               return StringUtil::getAnchorTag('https://www.facebook.com/'.$value, $value);
+               return StringUtil::getAnchorTag('https://www.facebook.com/'.$value, $value, true, true);
        }
 }
index d9fc30ccf9e07db37c9cfe52bbe2ecad7542402e..6d28096ec5aa0ea0f86445348be2d9b504ce5a02 100644 (file)
@@ -19,6 +19,6 @@ class TwitterUserOptionOutput implements IUserOptionOutput {
        public function getOutput(User $user, UserOption $option, $value) {
                if (empty($value)) return '';
                
-               return StringUtil::getAnchorTag('https://twitter.com/'.$value, $value);
+               return StringUtil::getAnchorTag('https://twitter.com/'.$value, $value, true, true);
        }
 }
index d0080932c1c2ca5b244ba1085d9153c98a7c7c24..0f2f1822dfff051645a5aca4b38cc821a3035670 100644 (file)
@@ -21,7 +21,7 @@ class URLUserOptionOutput implements IUserOptionOutput {
                
                $value = self::getURL($value);
                
-               return StringUtil::getAnchorTag($value, $value);
+               return StringUtil::getAnchorTag($value, $value, true, true);
        }
        
        /**
index bd7f21aa5232b58ba09658bf3e9efed0ee50c8cd..4fc465095a466289ffa404321e4b655ec3cc7b9a 100644 (file)
@@ -13,6 +13,7 @@ use wcf\util\StringUtil;
  * Optional parameter:
  *      `appendHref` (bool, default true)
  *     `appendClassname` (bool, default true)
+ *      `isUgc` (bool, default false)
  *
  * Usage:
  *     {anchorAttributes url=$url}
@@ -34,6 +35,7 @@ class AnchorAttributesFunctionTemplatePlugin implements IFunctionTemplatePlugin
                $url = $tagArgs['url'];
                $appendClassname = $tagArgs['appendClassname'] ?? true;
                $appendHref = $tagArgs['appendHref'] ?? true;
+               $isUgc = $tagArgs['isUgc'] ?? false;
                
                $external = true;
                if (ApplicationHandler::getInstance()->isInternalURL($url)) {
@@ -56,6 +58,9 @@ class AnchorAttributesFunctionTemplatePlugin implements IFunctionTemplatePlugin
                                $rel .= ' noopener noreferrer';
                                $attributes .= 'target="_blank"';
                        }
+                       if ($isUgc) {
+                               $rel .= ' ugc';
+                       }
                        
                        $attributes .= ' rel="' . $rel . '"';
                }
index d3708a0d4b6b5cbccec32357f0b01eea20abbc14..ecdc8301131a3b6c18b43df450d291582d070336 100644 (file)
@@ -667,9 +667,10 @@ final class StringUtil {
         * @param       string          $url
         * @param       string          $title
         * @param       boolean         $encodeTitle
+        * @param       boolean         $isUgc          true to add rel=ugc to the anchor tag 
         * @return      string          anchor tag
         */
-       public static function getAnchorTag($url, $title = '', $encodeTitle = true) {
+       public static function getAnchorTag($url, $title = '', $encodeTitle = true, $isUgc = false) {
                $url = self::trim($url);
                
                // cut visible url
@@ -684,17 +685,18 @@ final class StringUtil {
                        if (!$encodeTitle) $title = self::encodeHTML($title);
                }
                
-               return '<a '. self::getAnchorTagAttributes($url) .'>' . ($encodeTitle ? self::encodeHTML($title) : $title) . '</a>';
+               return '<a '. self::getAnchorTagAttributes($url, $isUgc) .'>' . ($encodeTitle ? self::encodeHTML($title) : $title) . '</a>';
        }
        
        /**
         * Generates the attributes for an anchor tag from given URL.
         *
         * @param       string          $url
+        * @param       boolean         $isUgc          true to add rel=ugc to the attributes
         * @return      string          attributes
         * @since       5.3
         */
-       public static function getAnchorTagAttributes($url) {
+       public static function getAnchorTagAttributes($url, $isUgc = false) {
                $external = true;
                if (ApplicationHandler::getInstance()->isInternalURL($url)) {
                        $external = false;
@@ -709,6 +711,9 @@ final class StringUtil {
                                $rel .= ' noopener noreferrer';
                                $attributes .= 'target="_blank"';
                        }
+                       if ($isUgc) {
+                               $rel .= ' ugc';
+                       }
                        
                        $attributes .= ' rel="' . $rel . '"';
                }