diff --git a/composer.json b/composer.json index 59bc8521..7261341b 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "erusev/parsedown": "^1.6", "erusev/parsedown-extra": "^0.7.1", "pelago/emogrifier": "^2.0", - "mustangostang/spyc": "^0.6.2" + "mustangostang/spyc": "^0.6.2", + "paquettg/php-html-parser": "^2.1" } } diff --git a/composer.lock b/composer.lock index 201ee40b..bd7cd8d0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4df89eeabf27a1fde648e75d29109826", + "content-hash": "f15420a8ce3f7350d05c6ac92f3e54b0", "packages": [ { "name": "erusev/parsedown", @@ -198,6 +198,102 @@ ], "time": "2017-02-24T16:06:33+00:00" }, + { + "name": "paquettg/php-html-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/paquettg/php-html-parser.git", + "reference": "d1000936350fed2cb6c54058890d2d19c5ccba4f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paquettg/php-html-parser/zipball/d1000936350fed2cb6c54058890d2d19c5ccba4f", + "reference": "d1000936350fed2cb6c54058890d2d19c5ccba4f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "paquettg/string-encode": "~1.0.0", + "php": ">=7.1" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.5.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPHtmlParser\\": "src/PHPHtmlParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gilles Paquette", + "email": "paquettg@gmail.com", + "homepage": "http://gillespaquette.ca" + } + ], + "description": "An HTML DOM parser. It allows you to manipulate HTML. Find tags on an HTML page with selectors just like jQuery.", + "homepage": "https://github.com/paquettg/php-html-parser", + "keywords": [ + "dom", + "html", + "parser" + ], + "time": "2019-08-18T18:27:45+00:00" + }, + { + "name": "paquettg/string-encode", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/paquettg/string-encoder.git", + "reference": "a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paquettg/string-encoder/zipball/a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee", + "reference": "a8708e9fac9d5ddfc8fc2aac6004e2cd05d80fee", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.1" + }, + "type": "library", + "autoload": { + "psr-0": { + "stringEncode": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gilles Paquette", + "email": "paquettg@gmail.com", + "homepage": "http://gillespaquette.ca" + } + ], + "description": "Facilitating the process of altering string encoding in PHP.", + "homepage": "https://github.com/paquettg/string-encoder", + "keywords": [ + "charset", + "encoding", + "string" + ], + "time": "2018-12-21T02:25:09+00:00" + }, { "name": "pelago/emogrifier", "version": "v2.2.0", diff --git a/controller/Controller.class.php b/controller/Controller.class.php index 55f0468f..ae7244d6 100644 --- a/controller/Controller.class.php +++ b/controller/Controller.class.php @@ -32,8 +32,11 @@ class Controller unset($viewParameters[View::LAYOUT_PARAMS]); $content = View::render($viewTemplate, $viewParameters + ['fullPage' => true]); - - Response::setContent($layout ? View::render('layout/basic', ['content' => $content] + $layoutParams) : $content); + if ($layout) { + $content = View::render('layout/basic', ['content' => $content] + $layoutParams); + } + $content = View::safeExternalLinks($content, Request::getHost()); + Response::setContent($content); } Response::setDefaultSecurityHeaders(); diff --git a/view/View.class.php b/view/View.class.php index 63dddb24..5ad91042 100644 --- a/view/View.class.php +++ b/view/View.class.php @@ -176,4 +176,33 @@ class View Response::setHeader(Response::HEADER_CONTENT_TYPE, 'application/json'); return ['internal/json', ['json' => $data, '_no_layout' => true]]; } + + public static function safeExternalLinks(string $html, string $domain): string + { + $dom = new PHPHtmlParser\Dom(); + $dom->load($html, ['cleanupInput' => false, 'removeDoubleSpace' => false, 'removeSmartyScripts' => false]); + + foreach ($dom->find('body a') as $link) + { + if (static::isLinkExternal($link->getAttribute('href'), $domain)) + { + $link->setAttribute('rel', "noopener noreferrer"); + } + } + return $dom->root->outerHtml(); + } + + public static function isLinkExternal(string $url, string $domain): bool + { + $components = parse_url(strtolower($url)); + $domain = strtolower($domain); + + return $url + && + !empty($components['host']) // relative urls are not external + && + $components['host'] !== $domain + && + mb_substr($components['host'], -mb_strlen('.' . $domain)) !== '.' . $domain; + } }