1<?php
2
3/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5/**
6 * Copyright (c) 2007-2009 Martin Jansen
7 *
8 * All rights reserved.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
12 * are met:
13 * 1. Redistributions of source code must retain the above copyright
14 *    notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 *    notice, this list of conditions and the following disclaimer in the
17 *    documentation and/or other materials provided with the distribution.
18 * 3. The name of the author may not be used to endorse or promote products
19 *    derived from this software without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 *
32 * @category Networking
33 * @package  URI_Template
34 * @author   Martin Jansen <mj@php.net>
35 * @license  http://www.opensource.org/licenses/bsd-license.php BSD
36 * @version  SVN: $Id: Template.php 294940 2010-02-12 00:11:48Z clockwerx $
37 * @link     http://pear.php.net/package/URI_Template
38 */
39
40/**
41 * Parser for URI Templates
42 *
43 * This class implements parsing of URI Templates as defined in the IETF's
44 * URI Template draft.
45 *
46 * @category  Networking
47 * @package   URI_Template
48 * @author    Martin Jansen <mj@php.net>
49 * @copyright 2007-2008 Martin Jansen
50 * @license   http://www.opensource.org/licenses/bsd-license.php BSD
51 * @version   Release: @package_version@
52 * @link      http://pear.php.net/package/URI_Template
53 * @since     Class available since release 0.1.0
54 */
55class URI_Template
56{
57    /**
58     * The URI template string.
59     *
60     * @var string $template URI template string
61     */
62    protected $template = '';
63
64    /**
65     * The array containing the replacement variables.
66     *
67     * @var array $values Array of replacement variables
68     */
69    protected $values = array();
70
71    /**
72     * Constructor method
73     *
74     * @param string $template URI Template string
75     */
76    public function __construct($template)
77    {
78        $this->template = $template;
79    }
80
81    /**
82     * Substitutes template expansions in the URI template.
83     *
84     * @param array   $values    Associative array with replacements for the
85     *                           variables in the expansions
86     * @param boolean $URLencode Boolean variable determing if the replacements
87     *                           should be encoded according to RFC 1738 or
88     *                           or not. Default is true, i.e. the replacements
89     *                           are encoded.
90     *
91     * @return string URI
92     */
93    public function substitute($values, $URLencode = true)
94    {
95        /* We need to assign $values to an object member because it is needed
96         * in self::_substitute().
97         */
98        $this->values = $values;
99
100        /* Because it is common that a template contains several replacements,
101         * we do the URL encoding here instead of in _substitute.
102         */
103        if ($URLencode) {
104            foreach ($this->values as &$value) {
105                if (is_array($value)) {
106                    $value = array_map('rawurlencode', $value);
107                } else {
108                    $value = rawurlencode($value);
109                }
110            }
111        }
112
113        return preg_replace_callback(
114            '~(\{[^\}]+\})~',
115            array($this, '_substitute'),
116            $this->template
117        );
118    }
119
120    /**
121     * Return an array containing the template variables names.
122     *
123     * @return array Array of template variables names
124     */
125    public function getTemplateVariables()
126    {
127        $variables = array();
128
129        if (preg_match_all('~(\{[^\}]+\})~', $this->template, $matches)) {
130            foreach ($matches[0] as $match) {
131                $expansion = substr($match, 1, -1);
132                list( , , $vars) = $this->parseExpansion($expansion);
133                $variables = array_merge($variables, array_keys($vars));
134            }
135            $variables = array_values(array_unique($variables));
136        }
137
138        return $variables;
139    }
140
141    /**
142     * Callback method for handling a single replacement.
143     *
144     * @param array $matches an Array of matched elements
145     *
146     * @see substitute
147     * @return string
148     */
149    protected function _substitute($matches)
150    {
151        $output = '';
152        $expansion = substr($matches[0], 1, -1);
153        list($op, $arg, $variables) = $this->parseExpansion($expansion);
154
155        foreach (array_keys($variables) as $key) {
156            if (isset($this->values[$key])) {
157                $variables[$key] = $this->values[$key];
158            }
159        }
160
161        if (!$op) {
162            $output = current($variables);
163        } else {
164            $opname = 'operation' . ucfirst(strtolower($op));
165            if (in_array($opname, get_class_methods($this))) {
166                $output = $this->$opname($variables, $arg);
167            }
168        }
169
170        return $output;
171    }
172
173    /**
174     * Implements the 'prefix' operator.
175     *
176     * Adds the value of the second parameter to the beginning of the first
177     * element from the first parameter and returns the resulting string.
178     * The value of the second parameter may be an array.
179     *
180     * @param array  $variables List of variables. Only the first element is
181     *                          used.
182     * @param string $arg       Prefix string
183     *
184     * @return string
185     */
186    protected function operationPrefix($variables, $arg)
187    {
188        $tmp = current($variables);
189        if (is_array($tmp)) {
190            if (count($tmp) > 0) {
191                $tmp = join($arg, $tmp);
192            } else {
193                $tmp = '';
194            }
195        }
196        return (empty($tmp) ? '' : $arg . $tmp);
197    }
198
199    /**
200     * Implements the 'suffix' operator.
201     *
202     * Appends the value of the second parameter to the first element of the
203     * first parameter and returns the resulting string.  The value of the
204     * second parameter may be an array.
205     *
206     * @param array  $variables List of variables. Only the first element is
207     *                          used.
208     * @param string $arg       String to append to the first element of
209     *                          $variables.
210     *
211     * @return string
212     */
213    protected function operationSuffix($variables, $arg)
214    {
215        $tmp = current($variables);
216        if (is_array($tmp)) {
217            if (count($tmp) > 0) {
218                $tmp = join($arg, $tmp);
219            } else {
220                $tmp = '';
221            }
222        }
223        return (empty($tmp) ? '' : $tmp . $arg);
224    }
225
226    /**
227     * Implements the 'join' operator.
228     *
229     * For each variable from the first parameter that is defined and
230     * non-empty create a keyvalue string that is the concatenation of the
231     * variable name, '=', and the variable value.  All elements are in turn
232     * concatenated with the value of the second parameter.
233     *
234     * @param array  $variables List of variables
235     * @param string $arg       Join needle
236     *
237     * @return string
238     */
239    protected function operationJoin($variables, $arg)
240    {
241        $tmp = array();
242        ksort($variables);
243        foreach ($variables as $key => $value) {
244            if (empty($value)) {
245                continue;
246            }
247
248            $tmp[] = $key . '=' . $value;
249        }
250
251        return join($arg, $tmp);
252    }
253
254    /**
255     * Implements the 'list' operator.
256     *
257     * Joins the elements of the first element of the first parameter with the
258     * value of the second parameter.
259     *
260     * @param array  $variables List of variables. Only the first element is
261     *                          used and this must be an array.
262     * @param string $arg       Join needle
263     *
264     * @return string
265     */
266    protected function operationList($variables, $arg)
267    {
268        $tmp = current($variables);
269        return (is_array($tmp) ? join($arg, $tmp) : '');
270    }
271
272    /**
273     * Implements the 'opt' operator.
274     *
275     * If one or more variables from the first parameter are non-empty then
276     * this method returns the value of the second parameter.  Otherwise an
277     * empty string is returned.
278     *
279     * @param array  $variables List of variables
280     * @param string $arg       Return value
281     *
282     * @return string
283     */
284    protected function operationOpt($variables, $arg)
285    {
286        foreach ($variables as $value) {
287            $defined = (is_array($value) ? (count($value) > 0) : !empty($value));
288            if ($defined) {
289                return $arg;
290            }
291        }
292
293        return '';
294    }
295
296    /**
297     * Implements the 'neg' operator.
298     *
299     * If all the variables from the first parameter are empty then this method
300     * returns the value of the second parameter.  Otherwise an empty string
301     * is returned.
302     *
303     * @param array  $variables List of variables
304     * @param string $arg       Return value
305     *
306     * @return string
307     */
308    protected function operationNeg($variables, $arg)
309    {
310        $defined = false;
311        foreach ($variables as $value) {
312            $defined = $defined || (!empty($value));
313        }
314
315        return (!$defined ? $arg : '');
316    }
317
318    /**
319     * Parses an expansion into its components
320     *
321     * @param string $expansion Expansion
322     *
323     * @see Appendix A of the URI Templates Draft
324     *      (http://bitworking.org/projects/URI-Templates/draft-gregorio-uritemplate-02.html#appendix_a)
325     * @return array Array with three elements containing the name of the
326     *               operation, the operation argument and the variables from
327     *               the expansion
328     */
329    protected function parseExpansion($expansion)
330    {
331        if (strstr($expansion, '|')) {
332            list($op, $arg, $vars) = explode('|', $expansion);
333            $op = substr($op, 1);
334        } else {
335            $op = $arg = '';
336            $vars = $expansion;
337        }
338
339        $vars = explode(',', $vars);
340
341        $variables = array();
342        foreach ($vars as $var) {
343            if (strstr($var, '=')) {
344                list($varname, $vardefault) = explode('=', $var);
345            } else {
346                $varname = $var;
347                $vardefault = '';
348            }
349
350            $variables[$varname] = $vardefault;
351        }
352
353        return array($op, $arg, $variables);
354    }
355}
356