1<?php 2 3 4namespace ComboStrap; 5 6/** 7 * Parse a wiki URL that you can found in the first part of a link 8 * 9 * This class takes care of the 10 * fact that a color can have a # 11 * and of the special syntax for an image 12 */ 13class DokuwikiUrl 14{ 15 16 /** 17 * In HTML (not in css) 18 * 19 * Because ampersands are used to denote HTML entities, 20 * if you want to use them as literal characters, you must escape them as entities, 21 * e.g. &. 22 * 23 * In HTML, Browser will do the translation for you if you give an URL 24 * not encoded but testing library may not and refuse them 25 * 26 * This URL encoding is mandatory for the {@link ml} function 27 * when there is a width and use them not otherwise 28 * 29 * Thus, if you want to link to: 30 * http://images.google.com/images?num=30&q=larry+bird 31 * you need to encode (ie pass this parameter to the {@link ml} function: 32 * http://images.google.com/images?num=30&q=larry+bird 33 * 34 * https://daringfireball.net/projects/markdown/syntax#autoescape 35 * 36 */ 37 const AMPERSAND_URL_ENCODED_FOR_HTML = '&'; 38 39 /** 40 * Used in dokuwiki syntax & in CSS attribute 41 * (Css attribute value are then HTML encoded as value of the attribute) 42 */ 43 const AMPERSAND_CHARACTER = "&"; 44 const ANCHOR_ATTRIBUTES = "anchor"; 45 /** 46 * @var array 47 */ 48 private $queryParameters; 49 /** 50 * @var false|string 51 */ 52 private $pathOrId; 53 /** 54 * @var false|string 55 */ 56 private $fragment; 57 /** 58 * @var false|string 59 */ 60 private $queryString; 61 62 /** 63 * Url constructor. 64 */ 65 public function __construct($url) 66 { 67 $this->queryParameters = []; 68 69 /** 70 * Path 71 */ 72 $questionMarkPosition = strpos($url, "?"); 73 $this->pathOrId = $url; 74 $queryStringAndAnchorOriginal = null; 75 if ($questionMarkPosition !== false) { 76 $this->pathOrId = substr($url, 0, $questionMarkPosition); 77 $queryStringAndAnchorOriginal = substr($url, $questionMarkPosition + 1); 78 } else { 79 // We may have only an anchor 80 $hashTagPosition = strpos($url, "#"); 81 if ($hashTagPosition !== false) { 82 $this->pathOrId = substr($url, 0, $hashTagPosition); 83 $this->fragment = substr($url, $hashTagPosition + 1); 84 } 85 } 86 87 /** 88 * Parsing Query string if any 89 */ 90 if ($queryStringAndAnchorOriginal !== null) { 91 92 /** 93 * The value $queryStringAndAnchorOriginal 94 * is kept to create the original queryString 95 * at the end if we found an anchor 96 */ 97 $queryStringAndAnchorProcessing = $queryStringAndAnchorOriginal; 98 while (strlen($queryStringAndAnchorProcessing) > 0) { 99 100 /** 101 * Capture the token 102 * and reduce the text 103 */ 104 $questionMarkPos = strpos($queryStringAndAnchorProcessing, "&"); 105 if ($questionMarkPos !== false) { 106 $token = substr($queryStringAndAnchorProcessing, 0, $questionMarkPos); 107 $queryStringAndAnchorProcessing = substr($queryStringAndAnchorProcessing, $questionMarkPos + 1); 108 } else { 109 $token = $queryStringAndAnchorProcessing; 110 $queryStringAndAnchorProcessing = ""; 111 } 112 113 114 /** 115 * Sizing (wxh) 116 */ 117 $sizing = []; 118 if (preg_match('/^([0-9]+)(?:x([0-9]+))?/', $token, $sizing)) { 119 $this->queryParameters[Dimension::WIDTH_KEY] = $sizing[1]; 120 if (isset($sizing[2])) { 121 $this->queryParameters[Dimension::HEIGHT_KEY] = $sizing[2]; 122 } 123 $token = substr($token, strlen($sizing[0])); 124 if ($token == "") { 125 // no anchor behind we continue 126 continue; 127 } 128 } 129 130 /** 131 * Linking 132 */ 133 $found = preg_match('/^(nolink|direct|linkonly|details)/i', $token, $matches); 134 if ($found) { 135 $linkingValue = $matches[1]; 136 $this->queryParameters[MediaLink::LINKING_KEY] = $linkingValue; 137 $token = substr($token, strlen($linkingValue)); 138 if ($token == "") { 139 // no anchor behind we continue 140 continue; 141 } 142 } 143 144 /** 145 * Cache 146 */ 147 $found = preg_match('/^(nocache)/i', $token, $matches); 148 if ($found) { 149 $cacheValue = "nocache"; 150 $this->queryParameters[CacheMedia::CACHE_KEY] = $cacheValue; 151 $token = substr($token, strlen($cacheValue)); 152 if ($token == "") { 153 // no anchor behind we continue 154 continue; 155 } 156 } 157 158 /** 159 * Anchor value after a single token case 160 */ 161 if (strpos($token, '#') === 0) { 162 $this->fragment = substr($token, 1); 163 continue; 164 } 165 166 /** 167 * Key, value 168 * explode to the first `=` 169 * in the anchor value, we can have one 170 * 171 * Ex with media.pdf#page=31 172 */ 173 list($key, $value) = explode("=", $token, 2); 174 175 /** 176 * Case of an anchor after a boolean attribute (ie without =) 177 * at the end 178 */ 179 $anchorPosition = strpos($key, '#'); 180 if ($anchorPosition !== false) { 181 $this->fragment = substr($key, $anchorPosition + 1); 182 $key = substr($key, 0, $anchorPosition); 183 } 184 185 /** 186 * Test Anchor on the value 187 */ 188 if($value!=null) { 189 if (($countHashTag = substr_count($value, "#")) >= 3) { 190 LogUtility::msg("The value ($value) of the key ($key) for the link ($this->pathOrId) has $countHashTag `#` characters and the maximum supported is 2.", LogUtility::LVL_MSG_ERROR); 191 continue; 192 } 193 } else { 194 /** 195 * Boolean attribute 196 */ 197 $value = "true"; 198 } 199 200 $anchorPosition = false; 201 $lowerCaseKey = strtolower($key); 202 if ($lowerCaseKey === TextColor::CSS_ATTRIBUTE) { 203 /** 204 * Special case when color has one color value as hexadecimal # 205 * and the hashtag 206 */ 207 if (strpos($value, '#') == 0) { 208 if (substr_count($value, "#") >= 2) { 209 210 /** 211 * The last one 212 */ 213 $anchorPosition = strrpos($value, '#'); 214 } 215 // no anchor then 216 } else { 217 // a color that is not hexadecimal can have an anchor 218 $anchorPosition = strpos($value, "#"); 219 } 220 } else { 221 // general case 222 $anchorPosition = strpos($value, "#"); 223 } 224 if ($anchorPosition !== false) { 225 $this->fragment = substr($value, $anchorPosition + 1); 226 $value = substr($value, 0, $anchorPosition); 227 } 228 229 switch ($lowerCaseKey) { 230 case "w": // used in a link w=xxx 231 $this->queryParameters[Dimension::WIDTH_KEY] = $value; 232 break; 233 case "h": // used in a link h=xxxx 234 $this->queryParameters[Dimension::HEIGHT_KEY] = $value; 235 break; 236 default: 237 /** 238 * Multiple parameter can be set to form an array 239 * 240 * Example: s=word1&s=word2 241 * 242 */ 243 if (isset($this->queryParameters[$key])){ 244 $actualValue = $this->queryParameters[$key]; 245 if(is_array($actualValue)){ 246 $actualValue[]=$value; 247 $this->queryParameters[$key] = $actualValue; 248 } else { 249 $this->queryParameters[$key] = [$actualValue, $value]; 250 } 251 } else { 252 $this->queryParameters[$key] = $value; 253 } 254 } 255 256 } 257 258 /** 259 * If a fragment was found, 260 * calculate the query string 261 */ 262 $this->queryString = $queryStringAndAnchorOriginal; 263 if ($this->fragment != null) { 264 $this->queryString = substr($queryStringAndAnchorOriginal, 0, -strlen($this->fragment) - 1); 265 } 266 } 267 268 } 269 270 271 public static function createFromUrl($dokuwikiUrl): DokuwikiUrl 272 { 273 return new DokuwikiUrl($dokuwikiUrl); 274 } 275 276 /** 277 * All URL token in an array 278 * @return array 279 */ 280 public function toArray(): array 281 { 282 $attributes = []; 283 $attributes[self::ANCHOR_ATTRIBUTES] = $this->fragment; 284 $attributes[PagePath::PROPERTY_NAME] = $this->pathOrId; 285 return PluginUtility::mergeAttributes($attributes, $this->queryParameters); 286 } 287 288 public function getQueryString() 289 { 290 return $this->queryString; 291 } 292 293 public function hasQueryParameter($propertyKey): bool 294 { 295 return isset($this->queryParameters[$propertyKey]); 296 } 297 298 public function getQueryParameters(): array 299 { 300 return $this->queryParameters; 301 } 302 303 public function getFragment() 304 { 305 return $this->fragment; 306 } 307 308 /** 309 * In Dokuwiki, a path may also be in the form of an id (ie without root separator) 310 * @return false|string 311 */ 312 public function getPath() 313 { 314 return $this->pathOrId; 315 } 316 317 public function getQueryParameter($key) 318 { 319 if(isset($this->queryParameters[$key])){ 320 return $this->queryParameters[$key]; 321 } else { 322 return null; 323 } 324 325 } 326 327 public function getScheme(): string 328 { 329 if(link_isinterwiki($this->pathOrId)){ 330 return InterWikiPath::scheme; 331 } 332 if(media_isexternal($this->pathOrId)){ 333 return InternetPath::scheme; 334 } 335 return DokuFs::SCHEME; 336 337 } 338 339 340} 341