--- /dev/null
+<?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 . "'.");
+ }
+}