"reserved", "/" => "segments", "." => "dotprefix", "#" => "fragment", ";" => "semicolon", "?" => "form", "&" => "continuation"]; /** * @var array * These are the characters which should not be URL encoded in reserved * strings. */ private $reserved = ["=", ",", "!", "@", "|", ":", "/", "?", "#", "[", "]", '$', "&", "'", "(", ")", "*", "+", ";"]; private $reservedEncoded = ["%3D", "%2C", "%21", "%40", "%7C", "%3A", "%2F", "%3F", "%23", "%5B", "%5D", "%24", "%26", "%27", "%28", "%29", "%2A", "%2B", "%3B"]; public function parse($string, array $parameters) { return $this->resolveNextSection($string, $parameters); } /** * This function finds the first matching {...} block and * executes the replacement. It then calls itself to find * subsequent blocks, if any. */ private function resolveNextSection($string, $parameters) { $start = \strpos($string, "{"); if ($start === \false) { return $string; } $end = \strpos($string, "}"); if ($end === \false) { return $string; } $string = $this->replace($string, $start, $end, $parameters); return $this->resolveNextSection($string, $parameters); } private function replace($string, $start, $end, $parameters) { // We know a data block will have {} round it, so we can strip that. $data = \substr($string, $start + 1, $end - $start - 1); // If the first character is one of the reserved operators, it effects // the processing of the stream. if (isset($this->operators[$data[0]])) { $op = $this->operators[$data[0]]; $data = \substr($data, 1); $prefix = ""; $prefix_on_missing = \false; switch ($op) { case "reserved": // Reserved means certain characters should not be URL encoded $data = $this->replaceVars($data, $parameters, ",", null, \true); break; case "fragment": // Comma separated with fragment prefix. Bare values only. $prefix = "#"; $prefix_on_missing = \true; $data = $this->replaceVars($data, $parameters, ",", null, \true); break; case "segments": // Slash separated data. Bare values only. $prefix = "/"; $data = $this->replaceVars($data, $parameters, "/"); break; case "dotprefix": // Dot separated data. Bare values only. $prefix = "."; $prefix_on_missing = \true; $data = $this->replaceVars($data, $parameters, "."); break; case "semicolon": // Semicolon prefixed and separated. Uses the key name $prefix = ";"; $data = $this->replaceVars($data, $parameters, ";", "=", \false, \true, \false); break; case "form": // Standard URL format. Uses the key name $prefix = "?"; $data = $this->replaceVars($data, $parameters, "&", "="); break; case "continuation": // Standard URL, but with leading ampersand. Uses key name. $prefix = "&"; $data = $this->replaceVars($data, $parameters, "&", "="); break; } // Add the initial prefix character if data is valid. if ($data || $data !== \false && $prefix_on_missing) { $data = $prefix . $data; } } else { // If no operator we replace with the defaults. $data = $this->replaceVars($data, $parameters); } // This is chops out the {...} and replaces with the new section. return \substr($string, 0, $start) . $data . \substr($string, $end + 1); } private function replaceVars($section, $parameters, $sep = ",", $combine = null, $reserved = \false, $tag_empty = \false, $combine_on_empty = \true) { if (\strpos($section, ",") === \false) { // If we only have a single value, we can immediately process. return $this->combine($section, $parameters, $sep, $combine, $reserved, $tag_empty, $combine_on_empty); } else { // If we have multiple values, we need to split and loop over them. // Each is treated individually, then glued together with the // separator character. $vars = \explode(",", $section); return $this->combineList( $vars, $sep, $parameters, $combine, $reserved, \false, // Never emit empty strings in multi-param replacements $combine_on_empty ); } } public function combine($key, $parameters, $sep, $combine, $reserved, $tag_empty, $combine_on_empty) { $length = \false; $explode = \false; $skip_final_combine = \false; $value = \false; // Check for length restriction. if (\strpos($key, ":") !== \false) { list($key, $length) = \explode(":", $key); } // Check for explode parameter. if ($key[\strlen($key) - 1] == "*") { $explode = \true; $key = \substr($key, 0, -1); $skip_final_combine = \true; } // Define the list separator. $list_sep = $explode ? $sep : ","; if (isset($parameters[$key])) { $data_type = $this->getDataType($parameters[$key]); switch ($data_type) { case self::TYPE_SCALAR: $value = $this->getValue($parameters[$key], $length); break; case self::TYPE_LIST: $values = []; foreach ($parameters[$key] as $pkey => $pvalue) { $pvalue = $this->getValue($pvalue, $length); if ($combine && $explode) { $values[$pkey] = $key . $combine . $pvalue; } else { $values[$pkey] = $pvalue; } } $value = \implode($list_sep, $values); if ($value == '') { return ''; } break; case self::TYPE_MAP: $values = []; foreach ($parameters[$key] as $pkey => $pvalue) { $pvalue = $this->getValue($pvalue, $length); if ($explode) { $pkey = $this->getValue($pkey, $length); $values[] = $pkey . "=" . $pvalue; // Explode triggers = combine. } else { $values[] = $pkey; $values[] = $pvalue; } } $value = \implode($list_sep, $values); if ($value == '') { return \false; } break; } } elseif ($tag_empty) { // If we are just indicating empty values with their key name, return that. return $key; } else { // Otherwise we can skip this variable due to not being defined. return \false; } if ($reserved) { $value = \str_replace($this->reservedEncoded, $this->reserved, $value); } // If we do not need to include the key name, we just return the raw // value. if (!$combine || $skip_final_combine) { return $value; } // Else we combine the key name: foo=bar, if value is not the empty string. return $key . ($value != '' || $combine_on_empty ? $combine . $value : ''); } /** * Return the type of a passed in value */ private function getDataType($data) { if (\is_array($data)) { \reset($data); if (\key($data) !== 0) { return self::TYPE_MAP; } return self::TYPE_LIST; } return self::TYPE_SCALAR; } /** * Utility function that merges multiple combine calls * for multi-key templates. */ private function combineList($vars, $sep, $parameters, $combine, $reserved, $tag_empty, $combine_on_empty) { $ret = []; foreach ($vars as $var) { $response = $this->combine($var, $parameters, $sep, $combine, $reserved, $tag_empty, $combine_on_empty); if ($response === \false) { continue; } $ret[] = $response; } return \implode($sep, $ret); } /** * Utility function to encode and trim values */ private function getValue($value, $length) { if ($length) { $value = \substr($value, 0, $length); } $value = \rawurlencode($value); return $value; } }