From 27930682fa9c590aeafc86992ea3f186cf2111a3 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 15 Jul 2017 12:37:00 +0200 Subject: [PATCH] Added `Url` wrapper for sane URL parsing See #2336 --- .../files/js/WoltLabSuite/Core/Devtools.js | 4 + .../server/PackageUpdateServer.class.php | 18 +- .../user/avatar/UserAvatarAction.class.php | 3 +- .../files/lib/page/AbstractPage.class.php | 5 +- .../bbcode/KeywordHighlighter.class.php | 3 +- .../output/node/HtmlOutputNodeImg.class.php | 5 +- .../files/lib/util/HTTPRequest.class.php | 6 +- wcfsetup/install/files/lib/util/Url.class.php | 157 ++++++++++++++++++ 8 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 wcfsetup/install/files/lib/util/Url.class.php diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Devtools.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Devtools.js index 9c57903da1..a749410976 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Devtools.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Devtools.js @@ -11,8 +11,12 @@ define([], function() { if (!COMPILER_TARGET_DEFAULT) { return { + help: function () {}, + toggleEditorAutosave: function () {}, toggleEventLogging: function () {}, _internal_: { + enable: function () {}, + editorAutosave: function () {}, eventLog: function() {} } }; diff --git a/wcfsetup/install/files/lib/data/package/update/server/PackageUpdateServer.class.php b/wcfsetup/install/files/lib/data/package/update/server/PackageUpdateServer.class.php index a94d0c287f..fd7e7dc82f 100644 --- a/wcfsetup/install/files/lib/data/package/update/server/PackageUpdateServer.class.php +++ b/wcfsetup/install/files/lib/data/package/update/server/PackageUpdateServer.class.php @@ -5,6 +5,7 @@ use wcf\system\io\RemoteFile; use wcf\system\Regex; use wcf\system\WCF; use wcf\util\FileUtil; +use wcf\util\Url; /** * Represents a package update server. @@ -76,18 +77,9 @@ class PackageUpdateServer extends DatabaseObject { * @return boolean */ public static function isValidServerURL($serverURL) { - if (trim($serverURL)) { - if (!$parsedURL = @parse_url($serverURL)) - return false; - if (!isset($parsedURL['scheme']) || ($parsedURL['scheme'] != 'http' && $parsedURL['scheme'] != 'https')) - return false; - if (!isset($parsedURL['host'])) - return false; - return true; - } - else { - return false; - } + $parsedURL = Url::parse($serverURL); + + return (in_array($parsedURL['scheme'], ['http', 'https']) && $parsedURL['host'] !== ''); } /** @@ -159,7 +151,7 @@ class PackageUpdateServer extends DatabaseObject { * @return string */ public function getHighlightedURL() { - $host = parse_url($this->serverURL, PHP_URL_HOST); + $host = Url::parse($this->serverURL)['host']; return str_replace($host, ''.$host.'', $this->serverURL); } diff --git a/wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php b/wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php index e439b4cbb1..862fa6a0cf 100644 --- a/wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php +++ b/wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php @@ -15,6 +15,7 @@ use wcf\system\WCF; use wcf\util\FileUtil; use wcf\util\HTTPRequest; use wcf\util\ImageUtil; +use wcf\util\Url; /** * Executes avatar-related actions. @@ -189,7 +190,7 @@ class UserAvatarAction extends AbstractDatabaseObjectAction { return; } - $tmp = parse_url($this->parameters['url']); + $tmp = Url::parse($this->parameters['url']); if (!isset($tmp['path'])) { @unlink($filename); return; diff --git a/wcfsetup/install/files/lib/page/AbstractPage.class.php b/wcfsetup/install/files/lib/page/AbstractPage.class.php index d38cc0942c..2a74ece8db 100644 --- a/wcfsetup/install/files/lib/page/AbstractPage.class.php +++ b/wcfsetup/install/files/lib/page/AbstractPage.class.php @@ -8,6 +8,7 @@ use wcf\system\request\RequestHandler; use wcf\system\WCF; use wcf\util\HeaderUtil; use wcf\util\StringUtil; +use wcf\util\Url; /** * Abstract implementation of a page which fires the default event actions of a @@ -187,7 +188,7 @@ abstract class AbstractPage implements IPage { // check if current request URL matches the canonical URL if ($this->canonicalURL && (empty($_POST) || $this->forceCanonicalURL)) { - $canonicalURL = parse_url(preg_replace('~[?&]s=[a-f0-9]{40}~', '', $this->canonicalURL)); + $canonicalURL = Url::parse(preg_replace('~[?&]s=[a-f0-9]{40}~', '', $this->canonicalURL)); // use $_SERVER['REQUEST_URI'] because it represents the URL used to access the site and not the internally rewritten one // IIS Rewrite-Module has a bug causing the REQUEST_URI to be ISO-encoded @@ -206,7 +207,7 @@ abstract class AbstractPage implements IPage { // reduce successive forwarded slashes into a single one $requestURI = preg_replace('~/{2,}~', '/', $requestURI); - $requestURL = parse_url($requestURI); + $requestURL = Url::parse($requestURI); $redirect = false; if ($canonicalURL['path'] != $requestURL['path']) { $redirect = true; diff --git a/wcfsetup/install/files/lib/system/bbcode/KeywordHighlighter.class.php b/wcfsetup/install/files/lib/system/bbcode/KeywordHighlighter.class.php index a42883576c..6cd6b30790 100644 --- a/wcfsetup/install/files/lib/system/bbcode/KeywordHighlighter.class.php +++ b/wcfsetup/install/files/lib/system/bbcode/KeywordHighlighter.class.php @@ -3,6 +3,7 @@ namespace wcf\system\bbcode; use wcf\system\SingletonFactory; use wcf\util\ArrayUtil; use wcf\util\StringUtil; +use wcf\util\Url; /** * Highlights keywords in text messages. @@ -65,7 +66,7 @@ class KeywordHighlighter extends SingletonFactory { } // take keywords from referer else if (!empty($_SERVER['HTTP_REFERER'])) { - $url = parse_url($_SERVER['HTTP_REFERER']); + $url = Url::parse($_SERVER['HTTP_REFERER']); if (!empty($url['query'])) { $query = explode('&', $url['query']); foreach ($query as $element) { diff --git a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php index 201c0924b6..e494420dc4 100644 --- a/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php +++ b/wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php @@ -9,6 +9,7 @@ use wcf\util\exception\CryptoException; use wcf\util\CryptoUtil; use wcf\util\DOMUtil; use wcf\util\StringUtil; +use wcf\util\Url; /** * Processes images. @@ -68,13 +69,13 @@ class HtmlOutputNodeImg extends AbstractHtmlOutputNode { $element->setAttribute('class', $class); if (MODULE_IMAGE_PROXY) { - $urlComponents = parse_url($src); - if ($urlComponents === false) { + if (!Url::is($src)) { // not a valid URL, discard it DOMUtil::removeNode($element); continue; } + $urlComponents = Url::parse($src); if (empty($urlComponents['host'])) { // relative URL, ignore it continue; diff --git a/wcfsetup/install/files/lib/util/HTTPRequest.class.php b/wcfsetup/install/files/lib/util/HTTPRequest.class.php index 407ce53540..6ea91cae5e 100644 --- a/wcfsetup/install/files/lib/util/HTTPRequest.class.php +++ b/wcfsetup/install/files/lib/util/HTTPRequest.class.php @@ -220,7 +220,7 @@ final class HTTPRequest { * @throws SystemException */ private function setURL($url) { - $parsedUrl = $originUrl = parse_url($url); + $parsedUrl = $originUrl = Url::parse($url); if (empty($originUrl['scheme']) || empty($originUrl['host'])) { throw new SystemException("Invalid URL '{$url}' given"); } @@ -238,8 +238,8 @@ final class HTTPRequest { $this->query = isset($parsedUrl['query']) ? $parsedUrl['query'] : ''; } - if (PROXY_SERVER_HTTP) { - $parsedUrl = parse_url(PROXY_SERVER_HTTP); + if (PROXY_SERVER_HTTP && Url::is(PROXY_SERVER_HTTP)) { + $parsedUrl = Url::parse(PROXY_SERVER_HTTP); } $this->useSSL = $parsedUrl['scheme'] === 'https'; diff --git a/wcfsetup/install/files/lib/util/Url.class.php b/wcfsetup/install/files/lib/util/Url.class.php new file mode 100644 index 0000000000..b312c3b7de --- /dev/null +++ b/wcfsetup/install/files/lib/util/Url.class.php @@ -0,0 +1,157 @@ + + * @package WoltLabSuite\Core\Util + * @since 3.1 + */ +final class Url implements \ArrayAccess { + /** + * list of url components + * @var string[] + */ + private $components = []; + + /** + * maps properties to the array indices + * @var integer[] + */ + private static $propertyMap = [ + PHP_URL_SCHEME => 'scheme', + PHP_URL_HOST => 'host', + PHP_URL_PORT => 'port', + PHP_URL_USER => 'user', + PHP_URL_PASS => 'pass', + PHP_URL_PATH => 'path', + PHP_URL_QUERY => 'query', + PHP_URL_FRAGMENT => 'fragment' + ]; + + /** + * Tests if provided url appears to be an url and can be processed by `parse_url()`. + * + * @param string $url + * @return boolean + */ + public static function is($url) { + return parse_url($url) !== false; + } + + /** + * Parses the provided url and returns an array containing all possible url + * components, even those not originally present, but in that case set to am + * 'empty' value. + * + * @param string $url + * @return Url + */ + public static function parse($url) { + $url = parse_url($url); + if ($url === false) $url = []; + + return new self([ + 'scheme' => (isset($url['scheme'])) ? $url['scheme'] : '', + 'host' => (isset($url['host'])) ? $url['host'] : '', + 'port' => (isset($url['port'])) ? $url['port'] : 0, + 'user' => (isset($url['user'])) ? $url['user'] : '', + 'pass' => (isset($url['pass'])) ? $url['pass'] : '', + 'path' => (isset($url['path'])) ? $url['path'] : '', + 'query' => (isset($url['query'])) ? $url['query'] : '', + 'fragment' => (isset($url['fragment'])) ? $url['fragment'] : '' + ]); + } + + /** + * Returns true if the provided url contains all listed components and + * that they're non-empty. + * + * @param string $url + * @param integer[] $components + * @return boolean + */ + public static function contains($url, array $components) { + $result = self::parse($url); + foreach ($components as $component) { + if (empty($result[$component])) { + return false; + } + } + + return true; + } + + /** + * Url constructor, object creation is only allowed through `Url::parse()`. + * + * @param string[] $components + */ + private function __construct(array $components) { + $this->components = $components; + } + + /** + * @inheritDoc + */ + public function offsetExists($offset) { + // We're throwing an exception here, if `$offset` is an unknown property + // key, which is a bit weird when working with `isset()` or `empty()`, + // but any unknown key is a guaranteed programming error. + // + // On top of that, we'll only return true, if the value is actually non- + // empty. That doesn't make much sense in combination with `isset()`, but + // instead is used to mimic the legacy behavior of the array returned by + // `parse_url()` with its missing keys. + return !empty($this->components[$this->getIndex($offset)]); + } + + /** + * @inheritDoc + */ + public function offsetGet($offset) { + return $this->components[$this->getIndex($offset)]; + } + + /** + * @inheritDoc + */ + public function offsetUnset($offset) { + throw new \RuntimeException("Url components are immutable"); + } + + /** + * @inheritDoc + */ + public function offsetSet($offset, $value) { + throw new \RuntimeException("Url components are immutable"); + } + + /** + * Attempts to resolve string properties and maps them to their integer-based + * component indices. Will throw an exception if the property is unknown, + * making it easier to spot typos. + * + * @param mixed $property + * @return integer + * @throws \RuntimeException + */ + private function getIndex($property) { + if (is_int($property) && isset(self::$propertyMap[$property])) { + return self::$propertyMap[$property]; + } + else if (is_string($property) && isset($this->components[$property])) { + return $property; + } + + throw new \RuntimeException("Unknown url component offset '" . $property . "'."); + } +} -- 2.20.1