Line data Source code
1 : /**
2 : Copyright (c) 2016-2019 Roman Katuntsev <sbkarr@stappler.org>
3 : Copyright (c) 2023 Stappler LLC <admin@stappler.dev>
4 :
5 : Permission is hereby granted, free of charge, to any person obtaining a copy
6 : of this software and associated documentation files (the "Software"), to deal
7 : in the Software without restriction, including without limitation the rights
8 : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 : copies of the Software, and to permit persons to whom the Software is
10 : furnished to do so, subject to the following conditions:
11 :
12 : The above copyright notice and this permission notice shall be included in
13 : all copies or substantial portions of the Software.
14 :
15 : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 : THE SOFTWARE.
22 : **/
23 :
24 : #include "SPUrl.h"
25 : #include "SPString.h"
26 : #include "SPStringView.h"
27 : #include "SPUrlTld.hpp"
28 :
29 : namespace STAPPLER_VERSIONIZED stappler {
30 :
31 : using Scheme = chars::Compose<char, chars::CharGroup<char, CharGroupId::Alphanumeric>, chars::Chars<char, '+', '-', '.'>>;
32 : using Ipv6 = chars::Compose<char, chars::CharGroup<char, CharGroupId::Hexadecimial>, chars::Chars<char, ':'>>;
33 :
34 : using Unreserved = chars::Compose<char, chars::CharGroup<char, CharGroupId::Alphanumeric>, chars::Chars<char, '-', '.', '_', '~', '%'>>;
35 : using SubDelim = chars::Chars<char, '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '='>;
36 : using GenDelim = chars::Chars<char, ':', '/', '?', '#', '[', ']', '@'>;
37 :
38 : using UnreservedUni = chars::Compose<char, Unreserved, chars::UniChar>;
39 :
40 1050 : static bool validateScheme(const StringView &r) {
41 1050 : auto cpy = r;
42 1050 : if (cpy.is<StringView::Compose<StringView::CharGroup<CharGroupId::Alphanumeric>, StringView::Chars<'.'>>>()) {
43 1050 : cpy ++;
44 1050 : cpy.skipChars<Scheme>();
45 1050 : if (cpy.empty()) {
46 1050 : return true;
47 : }
48 : }
49 0 : return false;
50 : }
51 :
52 4200 : static bool validateHost(StringView &r) {
53 4200 : if (r.empty()) {
54 125 : return false;
55 : }
56 4075 : auto cpy = r;
57 4075 : if (cpy.is('[')) {
58 : // check ipv6
59 100 : ++ cpy;
60 100 : cpy.skipChars<Ipv6>();
61 100 : if (cpy.is(']')) {
62 100 : cpy ++;
63 100 : if (cpy.empty()) {
64 100 : return true;
65 : }
66 : }
67 : } else {
68 3975 : cpy.skipChars<Unreserved, SubDelim, chars::UniChar>();
69 3975 : if (cpy.empty()) {
70 3950 : auto c = r.sub(r.size() - 1, 1);
71 3975 : while (c.is(',') || c.is('.') || c.is(';')) {
72 25 : r = r.sub(0, r.size() - 1);
73 25 : c = r.sub(r.size() - 1, 1);
74 : }
75 3950 : if (c.is<StringView::Compose<StringView::CharGroup<CharGroupId::Alphanumeric>, chars::UniChar>>()) {
76 3900 : StringView h(r);
77 :
78 3900 : if (!h.empty()) {
79 3900 : StringView domain;
80 10525 : while (!h.empty()) {
81 6650 : domain = h.readUntil<StringView::Chars<'.'>>();
82 6650 : if (h.is('.')) {
83 2775 : ++ h;
84 : }
85 :
86 6650 : if (domain.empty()) {
87 3600 : return false;
88 : }
89 : }
90 :
91 3875 : if (!domain.empty()) {
92 3875 : auto tmp = domain;
93 3875 : tmp.skipChars<StringView::CharGroup<CharGroupId::Alphanumeric>>();
94 3875 : if (!tmp.empty()) {
95 500 : if (UrlView::isValidIdnTld(domain)) {
96 3575 : return true;
97 : }
98 : } else {
99 3375 : return true;
100 : }
101 : }
102 : } else {
103 0 : return true;
104 : }
105 : }
106 : }
107 : }
108 375 : return false;
109 : }
110 :
111 900 : static bool validateUserOrPassword(const StringView &r) {
112 900 : auto cpy = r;
113 900 : cpy.skipChars<Unreserved, SubDelim, chars::UniChar>();
114 900 : if (cpy.empty()) {
115 900 : return true;
116 : }
117 0 : return false;
118 : }
119 :
120 7375 : bool UrlView::parseUrl(StringView &s, const Callback<void(StringViewUtf8, UrlView::UrlToken)> &cb) {
121 7375 : UrlView::UrlToken state = UrlView::UrlToken::Scheme;
122 :
123 7375 : StringView tmp;
124 7375 : if (s.is('[')) {
125 25 : state = UrlView::UrlToken::Host;
126 7350 : } else if (s.is("mailto:")) {
127 75 : cb(StringView(s, 6), UrlView::UrlToken::Scheme);
128 75 : s += 6;
129 75 : cb(StringView(s, 1), UrlView::UrlToken::Blank);
130 75 : ++ s;
131 75 : state = UrlView::UrlToken::User;
132 : } else {
133 7275 : tmp = s.readChars<UnreservedUni>();
134 : }
135 :
136 7375 : if (state == UrlView::UrlToken::Scheme) {
137 7275 : if (s.is(':')) {
138 : // scheme or host+port
139 1300 : if (tmp.empty()) {
140 0 : return false; // error
141 : }
142 :
143 1300 : if (s.is("://")) {
144 950 : if (!validateScheme(tmp)) {
145 0 : return false;
146 : }
147 :
148 950 : cb(tmp, UrlView::UrlToken::Scheme);
149 950 : cb(StringView(s, 3), UrlView::UrlToken::Blank);
150 950 : s += 3;
151 :
152 950 : if (s.is('[')) {
153 25 : state = UrlView::UrlToken::Host;
154 : } else {
155 925 : state = UrlView::UrlToken::User;
156 : }
157 : } else {
158 : // if it's port, next chars will be numbers only
159 350 : auto tmpS = s;
160 350 : tmpS ++;
161 350 : auto port = tmpS.readChars<StringView::CharGroup<CharGroupId::Numbers>>();
162 350 : if (!port.empty() && !tmpS.is<UnreservedUni>() && !tmpS.is('@')) {
163 : // host + port
164 175 : if (!validateHost(tmp)) {
165 100 : return true;
166 : }
167 :
168 175 : cb(tmp, UrlView::UrlToken::Host);
169 175 : cb(StringView(port.data() - 1, 1), UrlView::UrlToken::Blank);
170 175 : cb(port, UrlView::UrlToken::Port);
171 175 : s = tmpS;
172 :
173 175 : if (s.is('/')) {
174 25 : state = UrlView::UrlToken::Path;
175 150 : } else if (s.is<StringView::Chars<'?'>>()) {
176 25 : state = UrlView::UrlToken::Query;
177 125 : } else if (s.is('#')) {
178 25 : state = UrlView::UrlToken::Fragment;
179 : } else {
180 100 : return true;
181 : }
182 : } else {
183 175 : tmpS = s;
184 175 : ++ tmpS;
185 175 : auto arg = tmpS.readChars<UnreservedUni, SubDelim>();
186 175 : if (tmpS.is('@')) {
187 : // username + password
188 75 : if (!validateUserOrPassword(tmp) || !validateUserOrPassword(arg)) {
189 0 : return false;
190 : }
191 :
192 75 : cb(tmp, UrlView::UrlToken::User);
193 75 : cb(StringView(arg.data() - 1, 1), UrlView::UrlToken::Blank);
194 75 : cb(arg, UrlView::UrlToken::Password);
195 75 : cb(StringView(tmpS, 1), UrlView::UrlToken::Blank);
196 75 : state = UrlView::UrlToken::Host;
197 75 : ++ tmpS;
198 75 : s = tmpS;
199 : } else {
200 : // scheme without authority segment
201 100 : if (!validateScheme(tmp)) {
202 0 : return false;
203 : }
204 100 : cb(tmp, UrlView::UrlToken::Scheme);
205 100 : state = UrlView::UrlToken::Path;
206 100 : cb(s.sub(0, 1), UrlView::UrlToken::Blank);
207 100 : ++ s;
208 : }
209 : }
210 : }
211 5975 : } else if (s.is('@')) {
212 425 : if (tmp.empty() || !validateUserOrPassword(tmp)) {
213 0 : return false;
214 : }
215 425 : cb(tmp, UrlView::UrlToken::User);
216 425 : state = UrlView::UrlToken::Host;
217 425 : cb(StringView(s, 1), UrlView::UrlToken::Blank);
218 425 : ++ s;
219 5550 : } else if (s.is('/')) {
220 : // host + path
221 4900 : if (!tmp.empty()) {
222 850 : if (!validateHost(tmp)) {
223 300 : return false;
224 : }
225 550 : cb(tmp, UrlView::UrlToken::Host);
226 : }
227 4600 : state = UrlView::UrlToken::Path;
228 650 : } else if (s.is('?')) {
229 : // host + query
230 25 : if (tmp.empty()) {
231 0 : return false;
232 : }
233 25 : if (!validateHost(tmp)) {
234 0 : return false;
235 : }
236 25 : cb(tmp, UrlView::UrlToken::Host);
237 25 : state = UrlView::UrlToken::Query;
238 625 : } else if (s.is('#')) {
239 : // host + fragment
240 25 : if (tmp.empty()) {
241 0 : return false;
242 : }
243 25 : if (!validateHost(tmp)) {
244 0 : return false;
245 : }
246 25 : cb(tmp, UrlView::UrlToken::Host);
247 25 : state = UrlView::UrlToken::Fragment;
248 : } else {
249 : // assume host-only
250 600 : if (!tmp.empty()) {
251 600 : if (!validateHost(tmp)) {
252 0 : return false;
253 : }
254 600 : cb(tmp, UrlView::UrlToken::Host);
255 600 : return true;
256 : }
257 0 : return false;
258 : }
259 : }
260 :
261 6375 : if (state == UrlView::UrlToken::User) {
262 1000 : auto tmp_s = s;
263 1000 : tmp = tmp_s.readChars<UnreservedUni, SubDelim>();
264 :
265 1000 : if (tmp_s.is('@')) {
266 : // user only part
267 250 : if (!validateUserOrPassword(tmp)) {
268 75 : return false;
269 : }
270 250 : cb(tmp, UrlView::UrlToken::User);
271 250 : state = UrlView::UrlToken::Host;
272 250 : cb(StringView(tmp_s, 1), UrlView::UrlToken::Blank);
273 250 : ++ tmp_s;
274 250 : s = tmp_s;
275 750 : } else if (tmp_s.is(':')) {
276 : // user + password or host + port
277 125 : tmp_s ++;
278 125 : auto tmpS = tmp_s;
279 :
280 : // if it's port, next chars will be numbers only
281 125 : auto port = tmpS.readChars<StringView::CharGroup<CharGroupId::Numbers>>();
282 125 : if (!port.empty() && !tmpS.is('@')) {
283 : // host + port
284 75 : if (!validateHost(tmp)) {
285 0 : return true;
286 : }
287 :
288 75 : cb(tmp, UrlView::UrlToken::Host);
289 75 : cb(StringView(port.data() - 1, 1), UrlView::UrlToken::Blank);
290 75 : cb(port, UrlView::UrlToken::Port);
291 75 : s = tmpS;
292 :
293 75 : if (s.is('/')) {
294 25 : state = UrlView::UrlToken::Path;
295 50 : } else if (s.is('?')) {
296 25 : state = UrlView::UrlToken::Query;
297 25 : } else if (s.is('#')) {
298 25 : state = UrlView::UrlToken::Fragment;
299 : } else {
300 0 : return true;
301 : }
302 : } else {
303 : // user + password
304 50 : if (!validateUserOrPassword(tmp)) {
305 0 : return false;
306 : }
307 50 : cb(tmp, UrlView::UrlToken::User);
308 :
309 50 : if (tmpS.is('@')) {
310 25 : cb(StringView(port.data() - 1, 1), UrlView::UrlToken::Blank);
311 25 : cb(port, UrlView::UrlToken::Password);
312 25 : ++ tmpS;
313 25 : state = UrlView::UrlToken::Host;
314 25 : s = tmpS;
315 : } else {
316 25 : tmp = tmp_s.readChars<UnreservedUni, SubDelim>();
317 25 : if (!tmp_s.is('@')) {
318 0 : return false;
319 : }
320 25 : ++ tmp_s;
321 25 : if (!validateUserOrPassword(tmp)) {
322 0 : return false;
323 : }
324 25 : cb(StringView(tmp.data() - 1, 1), UrlView::UrlToken::Blank);
325 25 : cb(tmp, UrlView::UrlToken::Password);
326 25 : s = tmp_s;
327 25 : cb(StringView(s.data() - 1, 1), UrlView::UrlToken::Blank);
328 25 : state = UrlView::UrlToken::Host;
329 : }
330 : }
331 : } else {
332 : // host
333 625 : if (!validateHost(tmp)) {
334 0 : return false;
335 : }
336 :
337 625 : cb(tmp, UrlView::UrlToken::Host);
338 625 : s = tmp_s;
339 :
340 625 : if (tmp_s.is('/')) {
341 525 : state = UrlView::UrlToken::Path;
342 100 : } else if (tmp_s.is('?')) {
343 25 : state = UrlView::UrlToken::Query;
344 75 : } else if (tmp_s.is('#')) {
345 0 : state = UrlView::UrlToken::Fragment;
346 : } else {
347 75 : return true;
348 : }
349 : }
350 : }
351 :
352 6300 : if (state == UrlView::UrlToken::Host) {
353 850 : bool stop = false;
354 850 : if (s.is('[')) {
355 100 : auto t = s;
356 100 : ++ t;
357 100 : tmp = t.readChars<UnreservedUni, SubDelim, StringView::Chars<':'>>();
358 100 : if (t.is(']')) {
359 100 : ++ t;
360 100 : tmp = StringView(s.data(), t.data() - s.data());
361 100 : s = t;
362 : } else {
363 0 : return false;
364 : }
365 : } else {
366 750 : tmp = s.readChars<UnreservedUni, SubDelim, StringView::Chars<'[', ']'>>();
367 : }
368 850 : if (!validateHost(tmp)) {
369 125 : return false;
370 : }
371 725 : cb(tmp, UrlView::UrlToken::Host);
372 725 : if (s.is(':')) {
373 375 : auto tmp2 = s;
374 375 : ++ tmp2;
375 375 : auto port = tmp2.readChars<StringView::CharGroup<CharGroupId::Numbers>>();
376 375 : if (port.empty() || s.is<UnreservedUni>()) {
377 50 : state = UrlView::UrlToken::Path;
378 50 : stop = true;
379 : } else {
380 325 : cb(StringView(port.data() - 1, 1), UrlView::UrlToken::Blank);
381 325 : cb(port, UrlView::UrlToken::Port);
382 325 : s = tmp2;
383 : }
384 : }
385 725 : if (stop) {
386 : // do nothing
387 675 : } else if (s.is('/')) {
388 150 : state = UrlView::UrlToken::Path;
389 525 : } else if (s.is('?')) {
390 25 : state = UrlView::UrlToken::Query;
391 500 : } else if (s.is('#')) {
392 125 : state = UrlView::UrlToken::Fragment;
393 : } else {
394 375 : return true;
395 : }
396 : }
397 :
398 5800 : if (state == UrlView::UrlToken::Path) {
399 5475 : tmp = s.readChars<UnreservedUni, SubDelim, StringView::Chars<'/', ':', '@'>>();
400 5475 : if (!tmp.empty()) {
401 5475 : cb(tmp, UrlView::UrlToken::Path);
402 : }
403 :
404 5475 : if (s.is('?')) {
405 1400 : state = UrlView::UrlToken::Query;
406 4075 : } else if (s.is('#')) {
407 0 : state = UrlView::UrlToken::Fragment;
408 : } else {
409 4075 : return true;
410 : }
411 : }
412 :
413 1725 : if (state == UrlView::UrlToken::Query) {
414 1525 : tmp = s.readChars<UnreservedUni, SubDelim, StringView::Chars<'/', ':', '@', '?', '[', ']'>>();
415 1525 : if (!tmp.empty()) {
416 1525 : if (tmp.is('?')) {
417 1525 : cb(StringView(tmp, 1), UrlView::UrlToken::Blank);
418 1525 : ++ tmp;
419 : }
420 1525 : if (!tmp.empty()) {
421 1525 : cb(tmp, UrlView::UrlToken::Query);
422 : }
423 : }
424 :
425 1525 : if (s.is('#')) {
426 375 : state = UrlView::UrlToken::Fragment;
427 : } else {
428 1150 : return true;
429 : }
430 : }
431 :
432 575 : if (state == UrlView::UrlToken::Fragment) {
433 575 : tmp = s.readChars<UnreservedUni, SubDelim, StringView::Chars<'/', ':', '@', '?', '#', '[', ']'>>();
434 575 : if (!tmp.empty()) {
435 575 : if (tmp.is('#')) {
436 575 : cb(StringView(tmp, 1), UrlView::UrlToken::Blank);
437 575 : ++ tmp;
438 : }
439 575 : if (!tmp.empty()) {
440 575 : cb(tmp, UrlView::UrlToken::Fragment);
441 : }
442 : }
443 : }
444 :
445 575 : return true;
446 : }
447 :
448 : template <typename Vector>
449 6175 : auto _parsePath(StringView str, Vector &ret) {
450 6175 : StringView s(str);
451 : do {
452 14350 : if (s.is('/')) {
453 13425 : s ++;
454 : }
455 14350 : auto path = s.readUntil<StringView::Chars<'/', '?', ';', '&', '#'>>();
456 14350 : if (path == "..") {
457 0 : if (!ret.empty()) {
458 0 : ret.pop_back();
459 : }
460 14350 : } else if (path == ".") {
461 : // skip this component
462 : } else {
463 14350 : if (!path.empty()) {
464 13400 : ret.push_back(str);
465 : }
466 : }
467 14350 : } while (!s.empty() && s.is('/'));
468 6175 : }
469 :
470 : template <>
471 0 : auto UrlView::parsePath<memory::StandartInterface>(StringView str) -> memory::StandartInterface::VectorType<StringView> {
472 0 : memory::StandartInterface::VectorType<StringView> ret;
473 0 : _parsePath(str, ret);
474 0 : return ret;
475 0 : }
476 :
477 : template <>
478 6175 : auto UrlView::parsePath<memory::PoolInterface>(StringView str) -> memory::PoolInterface::VectorType<StringView> {
479 6175 : memory::PoolInterface::VectorType<StringView> ret;
480 6175 : _parsePath(str, ret);
481 6175 : return ret;
482 0 : }
483 :
484 550 : bool UrlView::isValidIdnTld(StringView str) {
485 550 : auto p = s_IdnTld;
486 61850 : while (*p) {
487 61550 : if (str == *p) {
488 250 : return true;
489 : }
490 61300 : ++ p;
491 : }
492 300 : return false;
493 : }
494 :
495 6375 : UrlView::UrlView() { }
496 :
497 1000 : UrlView::UrlView(StringView str) {
498 1000 : parse(str);
499 1000 : }
500 :
501 425 : void UrlView::clear() {
502 425 : scheme.clear();
503 425 : user.clear();
504 425 : password.clear();
505 425 : host.clear();
506 425 : port.clear();
507 425 : path.clear();
508 425 : query.clear();
509 425 : fragment.clear();
510 425 : url.clear();
511 425 : }
512 :
513 1050 : bool UrlView::parse(const StringView &str) {
514 1050 : StringView r(str);
515 2100 : return parse(r);
516 : }
517 :
518 7375 : bool UrlView::parse(StringView &str) {
519 7375 : auto tmp = str;
520 7375 : if (parseUrl(str, [this] (StringView str, UrlToken tok) {
521 17700 : switch (tok) {
522 1125 : case UrlToken::Scheme: scheme = str; break;
523 800 : case UrlToken::User: user = str; break;
524 125 : case UrlToken::Password: password = str; break;
525 2800 : case UrlToken::Host: host = str; break;
526 575 : case UrlToken::Port: port = str; break;
527 5475 : case UrlToken::Path: path = str; break;
528 1525 : case UrlToken::Query: query = str; break;
529 575 : case UrlToken::Fragment: fragment = str; break;
530 4700 : case UrlToken::Blank: break;
531 : }
532 17700 : })) {
533 6950 : url = StringView(tmp.data(), str.data() - tmp.data());
534 6950 : return true;
535 : } else {
536 425 : clear();
537 425 : return false;
538 : }
539 : }
540 :
541 2000 : static void UrlView_get(std::ostream &stream, const UrlView &view) {
542 2000 : if (!view.scheme.empty()) {
543 900 : stream << view.scheme << ":";
544 : }
545 2000 : if (!view.scheme.empty() && !view.host.empty() && view.scheme != "mailto") {
546 750 : stream << "//";
547 : }
548 2000 : if (!view.host.empty()) {
549 1825 : if (!view.user.empty()) {
550 425 : stream << view.user;
551 425 : if (!view.password.empty()) {
552 125 : stream << ":" << view.password;
553 : }
554 425 : stream << "@";
555 : }
556 1825 : stream << view.host;
557 1825 : if (!view.port.empty()) {
558 525 : stream << ":" << view.port;
559 : }
560 : }
561 2000 : if (!view.path.empty()) {
562 1050 : stream << view.path;
563 : }
564 2000 : if (!view.query.empty()) {
565 425 : stream << "?" << view.query;
566 : }
567 2000 : if (!view.fragment.empty()) {
568 575 : stream << "#" << view.fragment;
569 : }
570 2000 : }
571 :
572 : template <>
573 1775 : auto UrlView::get<memory::PoolInterface>() const -> memory::PoolInterface::StringType {
574 1775 : memory::PoolInterface::StringStreamType stream;
575 1775 : UrlView_get(stream, *this);
576 3550 : return stream.str();
577 1775 : }
578 :
579 : template <>
580 225 : auto UrlView::get<memory::StandartInterface>() const -> memory::StandartInterface::StringType {
581 225 : memory::StandartInterface::StringStreamType stream;
582 225 : UrlView_get(stream, *this);
583 450 : return stream.str();
584 225 : }
585 :
586 1775 : bool UrlView::isEmail() const {
587 1775 : return (!user.empty() && !host.empty()) && (scheme.empty() && password.empty() && port.empty() && path.empty() && query.empty() && fragment.empty());
588 : }
589 :
590 1575 : bool UrlView::isPath() const {
591 1575 : return !path.empty() && (scheme.empty() && user.empty() && password.empty() && host.empty() && port.empty() && query.empty() && fragment.empty());
592 : }
593 :
594 :
595 : #if MODULE_STAPPLER_DATA
596 :
597 : template <>
598 500 : auto UrlView::parseArgs<memory::PoolInterface>(StringView str, size_t maxVarSize) -> data::ValueTemplate<memory::PoolInterface> {
599 500 : if (str.empty()) {
600 0 : data::ValueTemplate<memory::PoolInterface>();
601 : }
602 500 : StringView r(str);
603 500 : if (r.front() == '?' || r.front() == '&' || r.front() == ';') {
604 0 : ++ r;
605 : }
606 1000 : return data::readUrlencoded<memory::PoolInterface>(str);
607 : }
608 :
609 : template <>
610 0 : auto UrlView::parseArgs<memory::StandartInterface>(StringView str, size_t maxVarSize) -> data::ValueTemplate<memory::StandartInterface> {
611 0 : if (str.empty()) {
612 0 : data::ValueTemplate<memory::StandartInterface>();
613 : }
614 0 : StringView r(str);
615 0 : if (r.front() == '?' || r.front() == '&' || r.front() == ';') {
616 0 : ++ r;
617 : }
618 0 : return data::readUrlencoded<memory::StandartInterface>(str);
619 : }
620 :
621 : #endif
622 :
623 :
624 : }
|