Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  /*
   3   * Copyright 2013 Google Inc.
   4   *
   5   * Licensed under the Apache License, Version 2.0 (the "License");
   6   * you may not use this file except in compliance with the License.
   7   * You may obtain a copy of the License at
   8   *
   9   *     http://www.apache.org/licenses/LICENSE-2.0
  10   *
  11   * Unless required by applicable law or agreed to in writing, software
  12   * distributed under the License is distributed on an "AS IS" BASIS,
  13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14   * See the License for the specific language governing permissions and
  15   * limitations under the License.
  16   */
  17  
  18  /**
  19   * Implementation of levels 1-3 of the URI Template spec.
  20   * @see http://tools.ietf.org/html/rfc6570
  21   */
  22  class Google_Utils_URITemplate
  23  {
  24    const TYPE_MAP = "1";
  25    const TYPE_LIST = "2";
  26    const TYPE_SCALAR = "4";
  27  
  28    /**
  29     * @var $operators array
  30     * These are valid at the start of a template block to
  31     * modify the way in which the variables inside are
  32     * processed.
  33     */
  34    private $operators = array(
  35        "+" => "reserved",
  36        "/" => "segments",
  37        "." => "dotprefix",
  38        "#" => "fragment",
  39        ";" => "semicolon",
  40        "?" => "form",
  41        "&" => "continuation"
  42    );
  43  
  44    /**
  45     * @var reserved array
  46     * These are the characters which should not be URL encoded in reserved
  47     * strings.
  48     */
  49    private $reserved = array(
  50        "=", ",", "!", "@", "|", ":", "/", "?", "#",
  51        "[", "]",'$', "&", "'", "(", ")", "*", "+", ";"
  52    );
  53    private $reservedEncoded = array(
  54      "%3D", "%2C", "%21", "%40", "%7C", "%3A", "%2F", "%3F",
  55      "%23", "%5B", "%5D", "%24", "%26", "%27", "%28", "%29",
  56      "%2A", "%2B", "%3B"
  57    );
  58  
  59    public function parse($string, array $parameters)
  60    {
  61      return $this->resolveNextSection($string, $parameters);
  62    }
  63  
  64    /**
  65     * This function finds the first matching {...} block and
  66     * executes the replacement. It then calls itself to find
  67     * subsequent blocks, if any.
  68     */
  69    private function resolveNextSection($string, $parameters)
  70    {
  71      $start = strpos($string, "{");
  72      if ($start === false) {
  73        return $string;
  74      }
  75      $end = strpos($string, "}");
  76      if ($end === false) {
  77        return $string;
  78      }
  79      $string = $this->replace($string, $start, $end, $parameters);
  80      return $this->resolveNextSection($string, $parameters);
  81    }
  82  
  83    private function replace($string, $start, $end, $parameters)
  84    {
  85      // We know a data block will have {} round it, so we can strip that.
  86      $data = substr($string, $start + 1, $end - $start - 1);
  87  
  88      // If the first character is one of the reserved operators, it effects
  89      // the processing of the stream.
  90      if (isset($this->operators[$data[0]])) {
  91        $op = $this->operators[$data[0]];
  92        $data = substr($data, 1);
  93        $prefix = "";
  94        $prefix_on_missing = false;
  95  
  96        switch ($op) {
  97          case "reserved":
  98            // Reserved means certain characters should not be URL encoded
  99            $data = $this->replaceVars($data, $parameters, ",", null, true);
 100            break;
 101          case "fragment":
 102            // Comma separated with fragment prefix. Bare values only.
 103            $prefix = "#";
 104            $prefix_on_missing = true;
 105            $data = $this->replaceVars($data, $parameters, ",", null, true);
 106            break;
 107          case "segments":
 108            // Slash separated data. Bare values only.
 109            $prefix = "/";
 110            $data =$this->replaceVars($data, $parameters, "/");
 111            break;
 112          case "dotprefix":
 113            // Dot separated data. Bare values only.
 114            $prefix = ".";
 115            $prefix_on_missing = true;
 116            $data = $this->replaceVars($data, $parameters, ".");
 117            break;
 118          case "semicolon":
 119            // Semicolon prefixed and separated. Uses the key name
 120            $prefix = ";";
 121            $data = $this->replaceVars($data, $parameters, ";", "=", false, true, false);
 122            break;
 123          case "form":
 124            // Standard URL format. Uses the key name
 125            $prefix = "?";
 126            $data = $this->replaceVars($data, $parameters, "&", "=");
 127            break;
 128          case "continuation":
 129            // Standard URL, but with leading ampersand. Uses key name.
 130            $prefix = "&";
 131            $data = $this->replaceVars($data, $parameters, "&", "=");
 132            break;
 133        }
 134  
 135        // Add the initial prefix character if data is valid.
 136        if ($data || ($data !== false && $prefix_on_missing)) {
 137          $data = $prefix . $data;
 138        }
 139  
 140      } else {
 141        // If no operator we replace with the defaults.
 142        $data = $this->replaceVars($data, $parameters);
 143      }
 144      // This is chops out the {...} and replaces with the new section.
 145      return substr($string, 0, $start) . $data . substr($string, $end + 1);
 146    }
 147  
 148    private function replaceVars(
 149        $section,
 150        $parameters,
 151        $sep = ",",
 152        $combine = null,
 153        $reserved = false,
 154        $tag_empty = false,
 155        $combine_on_empty = true
 156    ) {
 157      if (strpos($section, ",") === false) {
 158        // If we only have a single value, we can immediately process.
 159        return $this->combine(
 160            $section,
 161            $parameters,
 162            $sep,
 163            $combine,
 164            $reserved,
 165            $tag_empty,
 166            $combine_on_empty
 167        );
 168      } else {
 169        // If we have multiple values, we need to split and loop over them.
 170        // Each is treated individually, then glued together with the
 171        // separator character.
 172        $vars = explode(",", $section);
 173        return $this->combineList(
 174            $vars,
 175            $sep,
 176            $parameters,
 177            $combine,
 178            $reserved,
 179            false, // Never emit empty strings in multi-param replacements
 180            $combine_on_empty
 181        );
 182      }
 183    }
 184   
 185    public function combine(
 186        $key,
 187        $parameters,
 188        $sep,
 189        $combine,
 190        $reserved,
 191        $tag_empty,
 192        $combine_on_empty
 193    ) {
 194      $length = false;
 195      $explode = false;
 196      $skip_final_combine = false;
 197      $value = false;
 198  
 199      // Check for length restriction.
 200      if (strpos($key, ":") !== false) {
 201        list($key, $length) = explode(":", $key);
 202      }
 203      
 204      // Check for explode parameter.
 205      if ($key[strlen($key) - 1] == "*") {
 206        $explode = true;
 207        $key = substr($key, 0, -1);
 208        $skip_final_combine = true;
 209      }
 210      
 211      // Define the list separator.
 212      $list_sep = $explode ? $sep : ",";
 213      
 214      if (isset($parameters[$key])) {
 215        $data_type = $this->getDataType($parameters[$key]);
 216        switch ($data_type) {
 217          case self::TYPE_SCALAR:
 218            $value = $this->getValue($parameters[$key], $length);
 219            break;
 220          case self::TYPE_LIST:
 221            $values = array();
 222            foreach ($parameters[$key] as $pkey => $pvalue) {
 223              $pvalue = $this->getValue($pvalue, $length);
 224              if ($combine && $explode) {
 225                $values[$pkey] = $key . $combine . $pvalue;
 226              } else {
 227                $values[$pkey] = $pvalue;
 228              }
 229            }
 230            $value = implode($list_sep, $values);
 231            if ($value == '') {
 232              return '';
 233            }
 234            break;
 235          case self::TYPE_MAP:
 236            $values = array();
 237            foreach ($parameters[$key] as $pkey => $pvalue) {
 238              $pvalue = $this->getValue($pvalue, $length);
 239              if ($explode) {
 240                $pkey = $this->getValue($pkey, $length);
 241                $values[] = $pkey . "=" . $pvalue; // Explode triggers = combine.
 242              } else {
 243                $values[] = $pkey;
 244                $values[] = $pvalue;
 245              }
 246            }
 247            $value = implode($list_sep, $values);
 248            if ($value == '') {
 249              return false;
 250            }
 251            break;
 252        }
 253      } else if ($tag_empty) {
 254        // If we are just indicating empty values with their key name, return that.
 255        return $key;
 256      } else {
 257        // Otherwise we can skip this variable due to not being defined.
 258        return false;
 259      }
 260  
 261      if ($reserved) {
 262        $value = str_replace($this->reservedEncoded, $this->reserved, $value);
 263      }
 264  
 265      // If we do not need to include the key name, we just return the raw
 266      // value.
 267      if (!$combine || $skip_final_combine) {
 268        return $value;
 269      }
 270          
 271      // Else we combine the key name: foo=bar, if value is not the empty string.
 272      return $key . ($value != '' || $combine_on_empty ? $combine . $value : '');
 273    }
 274    
 275    /**
 276     * Return the type of a passed in value
 277     */
 278    private function getDataType($data)
 279    {
 280      if (is_array($data)) {
 281        reset($data);
 282        if (key($data) !== 0) {
 283          return self::TYPE_MAP;
 284        }
 285        return self::TYPE_LIST;
 286      }
 287      return self::TYPE_SCALAR;
 288    }
 289    
 290    /**
 291     * Utility function that merges multiple combine calls
 292     * for multi-key templates.
 293     */
 294    private function combineList(
 295        $vars,
 296        $sep,
 297        $parameters,
 298        $combine,
 299        $reserved,
 300        $tag_empty,
 301        $combine_on_empty
 302    ) {
 303      $ret = array();
 304      foreach ($vars as $var) {
 305        $response = $this->combine(
 306            $var,
 307            $parameters,
 308            $sep,
 309            $combine,
 310            $reserved,
 311            $tag_empty,
 312            $combine_on_empty
 313        );
 314        if ($response === false) {
 315          continue;
 316        }
 317        $ret[] = $response;
 318      }
 319      return implode($sep, $ret);
 320    }
 321    
 322    /**
 323     * Utility function to encode and trim values
 324     */
 325    private function getValue($value, $length)
 326    {
 327      if ($length) {
 328        $value = substr($value, 0, $length);
 329      }
 330      $value = rawurlencode($value);
 331      return $value;
 332    }
 333  }