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.  &amp;.
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&amp;q=larry+bird
33     *
34     * https://daringfireball.net/projects/markdown/syntax#autoescape
35     *
36     */
37    const AMPERSAND_URL_ENCODED_FOR_HTML = '&amp;';
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