Added `Url` wrapper for sane URL parsing
authorAlexander Ebert <ebert@woltlab.com>
Sat, 15 Jul 2017 10:37:00 +0000 (12:37 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 15 Jul 2017 10:37:00 +0000 (12:37 +0200)
See #2336

wcfsetup/install/files/js/WoltLabSuite/Core/Devtools.js
wcfsetup/install/files/lib/data/package/update/server/PackageUpdateServer.class.php
wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php
wcfsetup/install/files/lib/page/AbstractPage.class.php
wcfsetup/install/files/lib/system/bbcode/KeywordHighlighter.class.php
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php
wcfsetup/install/files/lib/util/HTTPRequest.class.php
wcfsetup/install/files/lib/util/Url.class.php [new file with mode: 0644]

index 9c57903da15176aeb5158c846dd12b3ef2ebbaaf..a7494109769cbd0df62045e359cc8b9dc629f88f 100644 (file)
@@ -11,8 +11,12 @@ define([], function() {
        
        if (!COMPILER_TARGET_DEFAULT) {
                return {
+                       help: function () {},
+                       toggleEditorAutosave: function () {},
                        toggleEventLogging: function () {},
                        _internal_: {
+                               enable: function () {},
+                               editorAutosave: function () {},
                                eventLog: function() {}
                        }
                };
index a94d0c287f334827305da8b8941e621e45ebd64a..fd7e7dc82f88b2e538e48494d5a041438f22a2cf 100644 (file)
@@ -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, '<strong>'.$host.'</strong>', $this->serverURL);
        }
        
index e439b4cbb176772b8336026ef025da9d55307d9b..862fa6a0cf9e87558efe6c127c79402c5c633573 100644 (file)
@@ -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;
index d38cc0942ce785d7de386bd64d6e8bac6957f658..2a74ece8db95ae05631572a2db2d37c8c0811236 100644 (file)
@@ -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;
index a42883576c5ab824a851e03d2c438dfddafc4310..6cd6b30790eb1423972d6227815780d3cae70d51 100644 (file)
@@ -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) {
index 201c0924b650c524dd5d8d9764f96ec9c2441abc..e494420dc49ffdbfaa1f43e3203cd30862435e46 100644 (file)
@@ -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;
index 407ce535403207e2e16dc5295a1328114176fd25..6ea91cae5e124c21daf31b4af24796fd59dcaa50 100644 (file)
@@ -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 (file)
index 0000000..b312c3b
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+namespace wcf\util;
+
+/**
+ * Generic wrapper around `parse_url()`.
+ * 
+ * Unlike the base function that is used during processing, the method `Url::parse()`
+ * will always provide a sane list of components, regardless if they're provided in
+ * the `parse_url()`-output. You'll still need to check if the desired parameters
+ * are non-empty.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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 . "'.");
+       }
+}