Subversion Repositories cheapmusic

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
103 - 1
<?php
2
namespace GuzzleHttp\Psr7;
3
 
4
use Psr\Http\Message\UriInterface;
5
 
6
/**
7
 * Resolves a URI reference in the context of a base URI and the opposite way.
8
 *
9
 * @author Tobias Schultze
10
 *
11
 * @link https://tools.ietf.org/html/rfc3986#section-5
12
 */
13
final class UriResolver
14
{
15
    /**
16
     * Removes dot segments from a path and returns the new path.
17
     *
18
     * @param string $path
19
     *
20
     * @return string
21
     * @link http://tools.ietf.org/html/rfc3986#section-5.2.4
22
     */
23
    public static function removeDotSegments($path)
24
    {
25
        if ($path === '' || $path === '/') {
26
            return $path;
27
        }
28
 
29
        $results = [];
30
        $segments = explode('/', $path);
31
        foreach ($segments as $segment) {
32
            if ($segment === '..') {
33
                array_pop($results);
34
            } elseif ($segment !== '.') {
35
                $results[] = $segment;
36
            }
37
        }
38
 
39
        $newPath = implode('/', $results);
40
 
41
        if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
42
            // Re-add the leading slash if necessary for cases like "/.."
43
            $newPath = '/' . $newPath;
44
        } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
45
            // Add the trailing slash if necessary
46
            // If newPath is not empty, then $segment must be set and is the last segment from the foreach
47
            $newPath .= '/';
48
        }
49
 
50
        return $newPath;
51
    }
52
 
53
    /**
54
     * Converts the relative URI into a new URI that is resolved against the base URI.
55
     *
56
     * @param UriInterface $base Base URI
57
     * @param UriInterface $rel  Relative URI
58
     *
59
     * @return UriInterface
60
     * @link http://tools.ietf.org/html/rfc3986#section-5.2
61
     */
62
    public static function resolve(UriInterface $base, UriInterface $rel)
63
    {
64
        if ((string) $rel === '') {
65
            // we can simply return the same base URI instance for this same-document reference
66
            return $base;
67
        }
68
 
69
        if ($rel->getScheme() != '') {
70
            return $rel->withPath(self::removeDotSegments($rel->getPath()));
71
        }
72
 
73
        if ($rel->getAuthority() != '') {
74
            $targetAuthority = $rel->getAuthority();
75
            $targetPath = self::removeDotSegments($rel->getPath());
76
            $targetQuery = $rel->getQuery();
77
        } else {
78
            $targetAuthority = $base->getAuthority();
79
            if ($rel->getPath() === '') {
80
                $targetPath = $base->getPath();
81
                $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
82
            } else {
83
                if ($rel->getPath()[0] === '/') {
84
                    $targetPath = $rel->getPath();
85
                } else {
86
                    if ($targetAuthority != '' && $base->getPath() === '') {
87
                        $targetPath = '/' . $rel->getPath();
88
                    } else {
89
                        $lastSlashPos = strrpos($base->getPath(), '/');
90
                        if ($lastSlashPos === false) {
91
                            $targetPath = $rel->getPath();
92
                        } else {
93
                            $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath();
94
                        }
95
                    }
96
                }
97
                $targetPath = self::removeDotSegments($targetPath);
98
                $targetQuery = $rel->getQuery();
99
            }
100
        }
101
 
102
        return new Uri(Uri::composeComponents(
103
            $base->getScheme(),
104
            $targetAuthority,
105
            $targetPath,
106
            $targetQuery,
107
            $rel->getFragment()
108
        ));
109
    }
110
 
111
    /**
112
     * Returns the target URI as a relative reference from the base URI.
113
     *
114
     * This method is the counterpart to resolve():
115
     *
116
     *    (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
117
     *
118
     * One use-case is to use the current request URI as base URI and then generate relative links in your documents
119
     * to reduce the document size or offer self-contained downloadable document archives.
120
     *
121
     *    $base = new Uri('http://example.com/a/b/');
122
     *    echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c'));  // prints 'c'.
123
     *    echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y'));  // prints '../x/y'.
124
     *    echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
125
     *    echo UriResolver::relativize($base, new Uri('http://example.org/a/b/'));   // prints '//example.org/a/b/'.
126
     *
127
     * This method also accepts a target that is already relative and will try to relativize it further. Only a
128
     * relative-path reference will be returned as-is.
129
     *
130
     *    echo UriResolver::relativize($base, new Uri('/a/b/c'));  // prints 'c' as well
131
     *
132
     * @param UriInterface $base   Base URI
133
     * @param UriInterface $target Target URI
134
     *
135
     * @return UriInterface The relative URI reference
136
     */
137
    public static function relativize(UriInterface $base, UriInterface $target)
138
    {
139
        if ($target->getScheme() !== '' &&
140
            ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '')
141
        ) {
142
            return $target;
143
        }
144
 
145
        if (Uri::isRelativePathReference($target)) {
146
            // As the target is already highly relative we return it as-is. It would be possible to resolve
147
            // the target with `$target = self::resolve($base, $target);` and then try make it more relative
148
            // by removing a duplicate query. But let's not do that automatically.
149
            return $target;
150
        }
151
 
152
        if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) {
153
            return $target->withScheme('');
154
        }
155
 
156
        // We must remove the path before removing the authority because if the path starts with two slashes, the URI
157
        // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also
158
        // invalid.
159
        $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
160
 
161
        if ($base->getPath() !== $target->getPath()) {
162
            return $emptyPathUri->withPath(self::getRelativePath($base, $target));
163
        }
164
 
165
        if ($base->getQuery() === $target->getQuery()) {
166
            // Only the target fragment is left. And it must be returned even if base and target fragment are the same.
167
            return $emptyPathUri->withQuery('');
168
        }
169
 
170
        // If the base URI has a query but the target has none, we cannot return an empty path reference as it would
171
        // inherit the base query component when resolving.
172
        if ($target->getQuery() === '') {
173
            $segments = explode('/', $target->getPath());
174
            $lastSegment = end($segments);
175
 
176
            return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment);
177
        }
178
 
179
        return $emptyPathUri;
180
    }
181
 
182
    private static function getRelativePath(UriInterface $base, UriInterface $target)
183
    {
184
        $sourceSegments = explode('/', $base->getPath());
185
        $targetSegments = explode('/', $target->getPath());
186
        array_pop($sourceSegments);
187
        $targetLastSegment = array_pop($targetSegments);
188
        foreach ($sourceSegments as $i => $segment) {
189
            if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) {
190
                unset($sourceSegments[$i], $targetSegments[$i]);
191
            } else {
192
                break;
193
            }
194
        }
195
        $targetSegments[] = $targetLastSegment;
196
        $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments);
197
 
198
        // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./".
199
        // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
200
        // as the first segment of a relative-path reference, as it would be mistaken for a scheme name.
201
        if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
202
            $relativePath = "./$relativePath";
203
        } elseif ('/' === $relativePath[0]) {
204
            if ($base->getAuthority() != '' && $base->getPath() === '') {
205
                // In this case an extra slash is added by resolve() automatically. So we must not add one here.
206
                $relativePath = ".$relativePath";
207
            } else {
208
                $relativePath = "./$relativePath";
209
            }
210
        }
211
 
212
        return $relativePath;
213
    }
214
 
215
    private function __construct()
216
    {
217
        // cannot be instantiated
218
    }
219
}