root/trunk/gregarius/extlib/uri_util.php

Revision 1641, 20.0 kB (checked in by mbonetti, 23 months ago)

Applied Dwayne C. Litzenberger's patch to better handler feed discovery on
relative URLs. (Thanks, Dwayne!)

Line 
1<?php
2# uri_util.php - URI utilities
3# Version 0.7  Sat, 25 Nov 2006 17:33:25 -0600
4#
5# Copyright (C) 2006  Dwayne C. Litzenberger <dlitz@dlitz.net>
6#
7# Permission is hereby granted, free of charge, to any person obtaining
8# a copy of this software and associated documentation files (the
9# "Software"), to deal in the Software without restriction, including
10# without limitation the rights to use, copy, modify, merge, publish,
11# distribute, sublicense, and/or sell copies of the Software, and to
12# permit persons to whom the Software is furnished to do so.
13#
14# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
26
27function parse_uri($uri)
28{
29    if (substr($uri, 0, 2) == '//') {
30        # Work around PHP parse_url bug.  PHP doesn't like URIs like
31        # "//example.com/foo" or "//example.com:80/foo"
32        $parts = parse_url('x-dummy-scheme:'.$uri);
33        unset($parts['scheme']);
34        return $parts;
35    }
36    return parse_url($uri);
37}
38
39function unparse_url($parts, $loose=false)
40{
41    return unparse_uri($parts, $loose);
42}
43
44# This function is indended to be the inverse of parse_url, with some optional
45# sanity checks against RFC 3986.
46function unparse_uri($parts, $loose=null) 
47{
48    if (is_null($loose)) { $loose = true; }
49
50    $p_scheme = @$parts['scheme'];
51    $p_host = @$parts['host'];
52    $p_port = @$parts['port'];
53    $p_user = @$parts['user'];
54    $p_pass = @$parts['pass'];
55    $p_path = @$parts['path'];
56    $p_query = @$parts['query'];
57    $p_fragment = @$parts['fragment'];
58   
59    if (!$loose) {
60        $dec_octet = '(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])';
61        $IPv4address = "(?:$dec_octet\\.$dec_octet\\.$dec_octet\\.$dec_octet)";
62        $h16 = '(?:[[:xdigit:]]{1,4})';
63        $ls32 = '(?:'.$h16.':'.$h16.'|'.$IPv4address.')';
64        $IPv6address = "(?:".
65                                     "(?:$h16:){6}$ls32|".
66                                   "::(?:$h16:){5}$ls32|".
67                              "$h16?::(?:$h16:){4}$ls32|".
68            "(?:(?:$h16:){0,1}$h16)?::(?:$h16:){3}$ls32|".
69            "(?:(?:$h16:){0,2}$h16)?::(?:$h16:){2}$ls32|".
70            "(?:(?:$h16:){0,3}$h16)?::(?:$h16:){1}$ls32|".
71            "(?:(?:$h16:){0,4}$h16)?::" .        "$ls32|".
72            "(?:(?:$h16:){0,5}$h16)?::" .        "$h16|".
73            "(?:(?:$h16:){0,6}$h16)?::".
74            ")";
75
76        $unreserved = '[[:alpha:]\d\-\._~]';
77        $sub_delims = "[!\$&'()\*\+,;=]";
78        $IPvFuture = "(?:v[[:xdigit:]]+\\.[$unreserved$sub_delims\\:]+)";
79        $IP_literal = "(?:\\[(?:$IPv6address|$IPvFuture)\\])";
80        $pct_encoded = "(?:%[[:xdigit:]]{2})";
81        $reg_name = "(?:$unreserved|$pct_encoded|$sub_delims)*";
82        $pchar = "(?:$unreserved|$pct_encoded|$sub_delims|\:@)";
83        $segment = "$pchar*";
84        $segment_nz = "$pchar+";
85        $path_absolute = "(?:/(?:$segment_nz(?:/$segment)*)?)";
86        $path_rootless = "$segment_nz(?:/$segment)*";
87       
88        # Validate the scheme part
89        #  scheme        = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
90        # NB: "file" is hard-coded in PHP
91        if (isset($p_scheme) and 
92            !preg_match('|^[[:alpha:]][[:alpha:]\d\+\-\.]*$|s', $p_scheme))
93        {
94            trigger_error('Illegal URI scheme', E_USER_WARNING);
95            return false;
96        }
97       
98        # Validate the host part
99        if (isset($p_host) and 
100            !preg_match("#^(?:$IP_literal|$IPv4address|$reg_name)\$#s",
101                $p_host))
102        {
103            trigger_error('Illegal host part', E_USER_WARNING);
104            return false;
105        }
106       
107        # Validate the port part
108        if (isset($p_port) and
109            !preg_match("#^\d*\$#s", $p_port))
110        {
111            trigger_error('Illegal port part', E_USER_WARNING);
112            return false;
113        }   
114       
115        # Validate the user part
116        if (isset($p_user) and
117            !preg_match("#^(?:$unreserved|$pct_encoded|$sub_delims)*\$#s",
118                $p_user))
119        {
120            trigger_error('Illegal user part', E_USER_WARNING);
121            return false;
122        }
123       
124        # Validate the password part
125        if (isset($p_pass) and
126            !preg_match("#^(?:$unreserved|$pct_encoded|$sub_delims|:)*\$#s",
127                $p_pass))
128        {
129            trigger_error('Illegal pass part', E_USER_WARNING);
130            return false;
131        }   
132       
133        # Validate the path part
134        if (isset($p_path) and
135            !preg_match("#^$path_absolute|$path_rootless\$#s", $p_path))
136        {
137            trigger_error('Illegal path part', E_USER_WARNING);
138            return false;
139        }   
140     
141        # Validate the query part
142        if (isset($p_query) and
143            !preg_match("#^(?:$pchar|/|\?)*\$#s", $p_query))
144        {
145            trigger_error('Illegal query part', E_USER_WARNING);
146            return false;
147        }   
148         
149        # Validate the fragment part
150        if (isset($p_fragment) and
151            !preg_match("#^(?:$pchar|/|\?)*\$#s", $p_fragment))
152        {
153            trigger_error('Illegal fragment part', E_USER_WARNING);
154            return false;
155        }   
156    }
157
158    # Build the URI
159    $retval = "";
160    if (isset($p_scheme)) {
161        $retval = $p_scheme . ":";
162        if (strtolower($p_scheme) == "file" and !isset($p_host)) {
163            $retval .= "//";
164        }
165    }
166    if (isset($p_host)) {
167        $retval .= "//";
168        if (isset($p_user) or isset($p_pass)) {
169            $retval .= isset($p_user) ? $p_user : "";
170            $retval .= isset($p_pass) ? ":" . $p_pass : "";
171            $retval .= '@';
172        }
173        $retval .= $p_host;
174        if (isset($p_port)) {
175            $retval .= ':' . $p_port;
176        }
177    }
178    if (isset($p_path)) {
179        $retval .= $p_path;
180    }
181    if (isset($p_query)) {
182        $retval .= '?' . $p_query;
183    }
184    if (isset($p_fragment)) {
185        $retval .= '#' . $p_fragment;
186    }
187    return $retval;
188}
189
190function get_current_url()
191{
192    $host = $_SERVER['SERVER_NAME'];
193    $port = $_SERVER['SERVER_PORT'];
194    $req_uri = $_SERVER['REQUEST_URI'];
195    $https = !empty($_SERVER['HTTPS']);
196    $parts = array(
197        'scheme' => ($https ? 'https' : 'http'),
198        'host' => $host,
199        'port' => $port);
200    if (($https and $port == 443) or (!$https and $port == 80)) {
201        unset($parts['port']);
202    }
203    $uri = unparse_url($parts) . $req_uri;
204    return $uri;
205}
206
207function absolute_url($uri, $base_absolute_uri=null)
208{
209    return absolute_uri($uri, $base_absolute_uri);
210}
211
212# See RFC 3986, section 5.2.
213# This is a "strict parser" for the purposes of the RFC.
214# Note that $base_absolute_uri MUST be an absolute URI, or null
215function absolute_uri($uri, $base_absolute_uri=null)
216{
217    if (is_null($base_absolute_uri)) {
218        $base_absolute_uri = get_current_url();
219    }
220   
221    # 5.2.1. Pre-parse the base URI
222    $base_absolute_uri = normalize_uri($base_absolute_uri);
223   
224    # 5.2.2 Transform References
225    $base = parse_uri($base_absolute_uri);
226    $r = parse_uri($uri);
227    $target = array();
228   
229    if (isset($r['scheme'])) {
230        $target['scheme'] = $r['scheme'];
231        $target['path'] = remove_dot_segments(remove_multiple_slashes($r['path']));
232        $target['query'] = @$r['query'];
233       
234        // conceptually, $target['authority'] = @$r['authority'];
235        $target['host'] = @$r['host'];
236        $target['port'] = @$r['port'];
237        $target['user'] = @$r['user'];
238        $target['pass'] = @$r['pass'];
239    } else {
240        // conceptually, if (isset($r['authority'))
241        if (isset($r['host'])) {
242            $target['path'] = remove_dot_segments(remove_multiple_slashes($r['path']));
243            $target['query'] = @$r['query'];
244           
245            // conceptually, $target['authority'] = @$r['authority'];
246            $target['host'] = @$r['host'];
247            $target['port'] = @$r['port'];
248            $target['user'] = @$r['user'];
249            $target['pass'] = @$r['pass'];
250        } else {
251            if (empty($r['path'])) {
252                $target['path'] = $base['path'];
253                if (isset($r['query'])) {
254                    $target['query'] = $r['query'];
255                } else {
256                    $target['query'] = $base['query'];
257                }
258            } else {
259                if (substr($r['path'], 0, 1) === '/') {
260                    $target['path'] = remove_dot_segments(remove_multiple_slashes($r['path']));
261                } else {
262                    // conceptually, $target['path'] = merge($base['path'], $r['path']);
263                    if (isset($base['host']) and empty($base['path'])) {
264                        $target['path'] = "/" . $r['path'];
265                    } else {
266                        $segs = explode('/', $base['path']);
267                        array_pop($segs);
268                        $target['path'] = implode('/', $segs) . "/" . $r['path'];
269                    }
270                   
271                    $target['path'] = remove_dot_segments(remove_multiple_slashes($target['path']));
272                }
273                $target['query'] = @$r['query'];
274            }
275           
276            // conceptually, $target['authority'] = @$r['authority'];
277            $target['host'] = @$base['host'];
278            $target['port'] = @$base['port'];
279            $target['user'] = @$base['user'];
280            $target['pass'] = @$base['pass'];
281        }
282        $target['scheme'] = @$base['scheme'];
283    }
284
285    $target['fragment'] = @$r['fragment'];
286
287    return unparse_uri($target);
288}
289
290
291# - When $no_empty_uri is true (the default), then zero-length relative URIs
292# (which indicate the "current document") will never be returned. This is to
293# avoid bugs in some programs.
294# - When $no_net_uri is true (the default), then network URIs (e.g.
295# "//www.example.com/foo") are never returned.  This is to avoid bugs in some
296# programs.
297
298function relative_uri($uri, $base_uri=null,
299    $no_empty_uri=null, $no_net_uri=null)
300{
301    if (is_null($base_uri)) { $base_uri = get_current_url(); }
302    if (is_null($no_empty_uri)) { $no_empty_uri = true; }
303    if (is_null($no_net_uri)) { $no_net_uri = true; }
304
305    $base_uri = absolute_uri($base_uri);
306    $uri = absolute_uri($uri, $base_uri);
307   
308    $base = parse_uri($base_uri);
309    $parts = parse_uri($uri);
310
311    do {
312        // scheme
313        if ($parts['scheme'] !== $base['scheme']) {
314            break;
315        }
316        if (!$no_net_uri) {
317            unset($parts['scheme']);
318        }
319
320        // authority
321        if ($parts['host'] !== $base['host'] or
322            $parts['user'] !== $base['user'] or
323            $parts['pass'] !== $base['pass'] or
324            $parts['port'] !== $base['port'])
325        {
326            break;
327        }
328        unset($parts['scheme']);
329        unset($parts['host']);
330        unset($parts['user']);
331        unset($parts['pass']);
332        unset($parts['port']);
333
334        // path
335        if ($parts['path'] === $base['path']) {
336            // Take just the basename of the path
337            $p = explode('/', $parts['path']);
338            if ($no_empty_uri) {
339                $parts['path'] = $p[count($p)-1];
340                if ($parts['path'] == '') {
341                    $parts['path'] = './';
342                }
343            } else {
344                $parts['path'] = '';
345            }
346
347            // query
348            if ($parts['query'] !== $base['query']) {
349                break;
350            }
351            unset($parts['query']);
352           
353            // fragment
354            if ($parts['fragment'] !== $base['fragment']) {
355                break;
356            }
357            unset($parts['fragment']);
358            break;
359        }
360
361        // Relative path calculation algorithm:
362        // We have two paths, the destination path (where we want to go), and
363        // the base path (where we are coming from).  So, we need to:
364        // 1. Find the deepest common parent between the two paths
365        // 2. Determine how many instances of "../" we need to get from the
366        // base path to the parent
367        // 3. Determine the relative path of the destination path with respect
368        // to the parent path, and append this to the "../" sequence.
369
370        // break down the path
371        $p = explode('/', $parts['path']);  // destination path
372        $bp = explode('/', $base['path']);  // base path
373       
374        // Determine the tree depth of each of the paths
375        $pDepth = count($p)-1;
376        $bpDepth = count($bp)-1;
377       
378        // 1. Find the depth of the deepest common parent (DCP) path between
379        // the two paths, noting that the final path element is not a directory.
380        $n = min(count($p), count($bp));
381        $dcpDepth = 0;
382        for($i = 1; $i < $n-1; $i++) {
383            if ($p[$i] !== $bp[$i]) {
384                break;
385            }
386            $dcpDepth = $i;
387        }
388       
389        // 2. Determine the number of "../"s needed to get from the base path
390        // to the DCP path.
391        $go_up = $bpDepth - $dcpDepth - 1;
392        if ($go_up > 0) {
393            $relpath = str_repeat('../', $go_up);
394        } else {
395            $relpath = '';
396        }
397       
398        // 3. Determine the relative path of the destination wrt the DCP path,
399        // and add it.
400        $relpath .= implode('/', array_slice($p, $dcpDepth + 1));
401       
402        if ($relpath == '') {
403            $relpath = './';
404        }
405        $parts['path'] = $relpath;
406       
407    } while(0);
408   
409   
410    return unparse_uri($parts);
411}
412
413// URI normalization.
414// See RFC 3986 section 6 (but note that we don't do everything specified
415// there.)
416function normalize_uri($uri) {
417    // Make sure file URIs have either 0 or 3 slashes after the colon
418    if (strtolower(substr($uri, 0, 6)) == 'file:/') {
419        $uri = substr($uri, 0, 5) . '///' . substr($uri, 6);
420    }
421
422    $u = parse_uri($uri);
423
424    // Convert the scheme name to lowercase
425    if (isset($u['scheme'])) {
426        $u['scheme'] = strtolower($u['scheme']);
427    }
428   
429    // Convert the host part to lowercase
430    if (isset($u['host'])) {
431        $u['host'] = strtolower($u['host']);
432    }
433   
434    // Remove multiple slashes (it's technically invalid URI syntax anyway)
435    $u['path'] = remove_multiple_slashes($u['path']);
436   
437    // 6.2.2.3. Path Segment Normalization (of absolute paths)
438    $u['path'] = remove_dot_segments($u['path']);
439
440    // 6.2.3. Scheme-Based Normalization
441    if (isset($u['scheme'])) {
442
443        if ($u['scheme'] == 'http') {
444            if (empty($u['port']) or $u['port'] == 80) {
445                unset($u['port']);
446            }
447           
448            if (empty($u['path'])) {
449                $u['path'] = '/';
450            }
451       
452        } elseif ($u['scheme'] == 'https') {
453            if (empty($u['port']) or $u['port'] == 443) {
454                unset($u['port']);
455            }
456           
457            if (empty($u['path'])) {
458                $u['path'] = '/';
459            }
460       
461        } elseif ($u['scheme'] == 'ftp') {
462            if (empty($u['port']) or $u['port'] == 21) {
463                unset($u['port']);
464            }
465           
466            if (empty($u['path'])) {
467                $u['path'] = '/';
468            }
469
470        }
471    }
472
473    return unparse_uri($u);
474}
475
476// Remove multiple slashes (it's technically invalid URI syntax anyway)
477function remove_multiple_slashes($path)
478{
479    while (strpos($path, '//') !== FALSE) {
480        $path = str_replace('//', '/', $path);
481    }
482    return $path;
483}
484
485// RFC 3986 section 5.2.4 remove_dot_segments algorithm
486function remove_dot_segments($uri)
487{
488    // Create an array of segments-or-slashes for the input buffer
489    $inbuf = array();
490    foreach(explode('/', $uri) as $seg) {
491        $inbuf[] = '/';
492        if (!empty($seg)) {
493            $inbuf[] = $seg;
494        }
495    }
496    array_shift($inbuf);
497
498    $outbuf = array();
499   
500    while(!empty($inbuf)) {
501        if (($inbuf[0] === '..' or $inbuf[0] === '.') and $inbuf[1] === '/') {
502            array_splice($inbuf, 0, 2);
503           
504        } elseif (array_slice($inbuf, 0, 2) === array('/', '.')) {
505            if ($inbuf[2] === '/') {
506                array_splice($inbuf, 0, 3, array('/'));
507            } else {
508                array_splice($inbuf, 0, 2, array('/'));
509            }
510       
511        } elseif (array_slice($inbuf, 0, 2) === array('/', '..')) {
512            if ($inbuf[2] === '/') {
513                array_splice($inbuf, 0, 3, array('/'));
514            } else {
515                array_splice($inbuf, 0, 2, array('/'));
516            }
517            if (!empty($outbuf)) {
518                array_pop($outbuf);
519                if (!empty($outbuf) and $outbuf[count($outbuf)-1] === '/') {
520                    array_pop($outbuf);
521                }
522            }
523        } elseif ($inbuf === array('.') or $inbuf === array('..')) {
524            array_pop($inbuf);
525       
526        } else {
527            if ($inbuf[0] === '/') {
528                array_splice($outbuf, count($outbuf), 0, array_splice($inbuf, 0, 2));
529            } else {
530                // equivalent to array_push($outbuf, array_shift($inbuf));
531                array_splice($outbuf, count($outbuf), 0, array_splice($inbuf, 0, 1));
532            }
533       
534        }
535       
536    }
537   
538    return implode('', $outbuf);
539}
540
541// Split and decode query string (foo=bar&blah=baz&...) into its constituent
542// parts
543function parse_query_string($qs)
544{
545    $qs = explode('&', $qs);
546    $retval = array();
547    foreach ($qs as $a) {
548        $b = explode('=', $a, 2);
549        $k = urldecode($b[0]);
550        $v = urldecode($b[1]);
551        $retval[$k] = $v;
552    }
553    return $retval;
554}
555
556function unparse_query_string($query)
557{
558    $qs = array();
559    foreach($query as $k => $v) {
560        $qs[] = rawurlencode($k) . '=' . rawurlencode($v);
561    }
562    return implode('&', $qs);
563}
564
565function set_query_string_vars($uri, $array)
566{
567    $parsed_uri = parse_uri($uri);
568    if (empty($parsed_uri['query'])) {
569        $qs = array();
570    } else {
571        $qs = parse_query_string($parsed_uri['query']);
572    }
573    foreach ($array as $key => $value) {
574        if (is_null($value)) {
575            unset($qs[$key]);
576        } else {
577            $qs[$key] = $value;
578        }
579    }
580    $parsed_uri['query'] = unparse_query_string($qs);
581    return unparse_uri($parsed_uri);
582}
583
584function set_query_string_var($uri, $key, $value)
585{
586    return set_query_string_vars($uri, array($key => $value));
587}
588
589function percent_encode($s)
590{
591    $retval = array();
592    $len = strlen($s);
593    for ($i = 0; $i < $len; $i++) {
594        $ch = substr($s, $i, 1);
595        $hexch = bin2hex($ch);
596        if (strlen($hexch) != 2) {
597            die("Multi-byte characters not supported