Passed
Push — master ( 4c1c1f...a4f8c1 )
by Maurício
08:40 queued 12s
created

Plugins::getPlugins()   C

Complexity

Conditions 13
Paths 9

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 13.292

Importance

Changes 0
Metric Value
cc 13
eloc 26
c 0
b 0
f 0
nc 9
nop 1
dl 0
loc 50
rs 6.6166
ccs 22
cts 25
cp 0.88
crap 13.292

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin;
6
7
use FilesystemIterator;
8
use PhpMyAdmin\Html\MySQLDocumentation;
9
use PhpMyAdmin\Plugins\AuthenticationPlugin;
10
use PhpMyAdmin\Plugins\ExportPlugin;
11
use PhpMyAdmin\Plugins\ImportPlugin;
12
use PhpMyAdmin\Plugins\Plugin;
13
use PhpMyAdmin\Plugins\SchemaPlugin;
14
use PhpMyAdmin\Properties\Options\Groups\OptionsPropertySubgroup;
15
use PhpMyAdmin\Properties\Options\Items\BoolPropertyItem;
16
use PhpMyAdmin\Properties\Options\Items\DocPropertyItem;
17
use PhpMyAdmin\Properties\Options\Items\HiddenPropertyItem;
18
use PhpMyAdmin\Properties\Options\Items\MessageOnlyPropertyItem;
19
use PhpMyAdmin\Properties\Options\Items\NumberPropertyItem;
20
use PhpMyAdmin\Properties\Options\Items\RadioPropertyItem;
21
use PhpMyAdmin\Properties\Options\Items\SelectPropertyItem;
22
use PhpMyAdmin\Properties\Options\Items\TextPropertyItem;
23
use PhpMyAdmin\Properties\Options\OptionsPropertyItem;
24
use SplFileInfo;
25
use Throwable;
26
27
use function __;
28
use function class_exists;
29
use function count;
30
use function get_class;
31
use function htmlspecialchars;
32
use function is_subclass_of;
33
use function mb_strpos;
34
use function mb_strtolower;
35
use function mb_strtoupper;
36
use function mb_substr;
37
use function method_exists;
38
use function preg_match_all;
39
use function sprintf;
40
use function str_replace;
41
use function str_starts_with;
42
use function strcasecmp;
43
use function strcmp;
44
use function strtolower;
45
use function ucfirst;
46
use function usort;
47
48
class Plugins
49
{
50
    /**
51
     * Instantiates the specified plugin type for a certain format
52
     *
53
     * @param string            $type   the type of the plugin (import, export, etc)
54
     * @param string            $format the format of the plugin (sql, xml, et )
55
     * @param array|string|null $param  parameter to plugin by which they can decide whether they can work
56
     * @psalm-param array{export_type: string, single_table: bool}|string|null $param
57
     *
58
     * @return object|null new plugin instance
59
     */
60
    public static function getPlugin(string $type, string $format, $param = null): ?object
61
    {
62
        $GLOBALS['plugin_param'] = $param;
63
        $pluginType = mb_strtoupper($type[0]) . mb_strtolower(mb_substr($type, 1));
64
        $pluginFormat = mb_strtoupper($format[0]) . mb_strtolower(mb_substr($format, 1));
65
        $class = sprintf('PhpMyAdmin\\Plugins\\%s\\%s%s', $pluginType, $pluginType, $pluginFormat);
66
        if (! class_exists($class)) {
67
            return null;
68
        }
69
70
        if ($type === 'export') {
71
            /**
72
             * @psalm-suppress InvalidArrayOffset, MixedAssignment, MixedMethodCall
73
             */
74
            return new $class(
75
                $GLOBALS['containerBuilder']->get('relation'),
76
                $GLOBALS['containerBuilder']->get('export'),
77
                $GLOBALS['containerBuilder']->get('transformations')
78
            );
79
        }
80
81
        return new $class();
82
    }
83
84
    /**
85
     * @param string $type server|database|table|raw
86
     *
87
     * @return ExportPlugin[]
88 4
     */
89
    public static function getExport(string $type, bool $singleTable): array
90 4
    {
91
        $GLOBALS['plugin_param'] = ['export_type' => $type, 'single_table' => $singleTable];
92 4
93
        return self::getPlugins('Export');
94
    }
95
96
    /**
97
     * @param string $type server|database|table
98
     *
99
     * @return ImportPlugin[]
100 4
     */
101
    public static function getImport(string $type): array
102 4
    {
103
        $GLOBALS['plugin_param'] = $type;
104 4
105
        return self::getPlugins('Import');
106
    }
107
108
    /**
109
     * @return SchemaPlugin[]
110 4
     */
111
    public static function getSchema(): array
112 4
    {
113
        return self::getPlugins('Schema');
114
    }
115
116
    /**
117
     * Reads all plugin information
118
     *
119
     * @param string $type the type of the plugin (import, export, etc)
120
     * @psalm-param 'Export'|'Import'|'Schema' $type
121
     *
122 12
     * @return Plugin[] list of plugin instances
123
     */
124
    private static function getPlugins(string $type): array
125 12
    {
126
        try {
127
            $files = new FilesystemIterator(ROOT_PATH . 'libraries/classes/Plugins/' . $type);
128
        } catch (Throwable $e) {
129
            return [];
130 12
        }
131
132
        $plugins = [];
133 12
134 12
        /** @var SplFileInfo $fileInfo */
135 12
        foreach ($files as $fileInfo) {
136
            if (! $fileInfo->isReadable() || ! $fileInfo->isFile() || $fileInfo->getExtension() !== 'php') {
137
                continue;
138 12
            }
139 8
140
            if (! str_starts_with($fileInfo->getFilename(), $type)) {
141
                continue;
142 12
            }
143 12
144
            $class = sprintf('PhpMyAdmin\\Plugins\\%s\\%s', $type, $fileInfo->getBasename('.php'));
145
            if (! class_exists($class) || ! is_subclass_of($class, Plugin::class) || ! $class::isAvailable()) {
146
                continue;
147 12
            }
148
149
            if ($type === 'Export') {
150
                /**
151 4
                 * @psalm-suppress InvalidArrayOffset, MixedAssignment, MixedMethodCall
152 4
                 */
153 4
                $plugin = new $class(
154 4
                    $GLOBALS['containerBuilder']->get('relation'),
155
                    $GLOBALS['containerBuilder']->get('export'),
156
                    $GLOBALS['containerBuilder']->get('transformations')
157 8
                );
158
            } else {
159
                $plugin = new $class();
160 12
            }
161 8
162
            if (! ($plugin instanceof Plugin) || ! $plugin->isAvailable()) {
163
                continue;
164 12
            }
165
166
            $plugins[] = $plugin;
167 9
        }
168 12
169 3
        usort($plugins, static function (Plugin $plugin1, Plugin $plugin2): int {
170
            return strcasecmp($plugin1->getProperties()->getText(), $plugin2->getProperties()->getText());
171 12
        });
172
173
        return $plugins;
174
    }
175
176
    /**
177
     * Returns locale string for $name or $name if no locale is found
178
     *
179
     * @param string|null $name for local string
180
     *
181 4
     * @return string  locale string for $name
182
     */
183 4
    public static function getString($name)
184
    {
185
        return $GLOBALS[$name] ?? $name ?? '';
186
    }
187
188
    /**
189
     * Returns html input tag option 'checked' if plugin $opt
190
     * should be set by config or request
191
     *
192
     * @param string $section name of config section in
193
     *                        $GLOBALS['cfg'][$section] for plugin
194
     * @param string $opt     name of option
195
     * @psalm-param 'Export'|'Import'|'Schema' $section
196
     *
197
     * @return string  html input tag option 'checked'
198
     */
199
    public static function checkboxCheck($section, $opt)
200
    {
201
        // If the form is being repopulated using $_GET data, that is priority
202
        if (
203
            isset($_GET[$opt])
204
            || ! isset($_GET['repopulate'])
205
            && ((! empty($GLOBALS['timeout_passed']) && isset($_REQUEST[$opt]))
206
                || ! empty($GLOBALS['cfg'][$section][$opt]))
207
        ) {
208
            return ' checked="checked"';
209
        }
210
211
        return '';
212
    }
213
214
    /**
215
     * Returns default value for option $opt
216
     *
217
     * @param string $section name of config section in
218
     *                        $GLOBALS['cfg'][$section] for plugin
219
     * @param string $opt     name of option
220
     * @psalm-param 'Export'|'Import'|'Schema' $section
221
     *
222 32
     * @return string  default value for option $opt
223
     */
224 32
    public static function getDefault($section, $opt)
225
    {
226 8
        if (isset($_GET[$opt])) {
227
            // If the form is being repopulated using $_GET data, that is priority
228
            return htmlspecialchars($_GET[$opt]);
229 24
        }
230 4
231
        if (isset($GLOBALS['timeout_passed'], $_REQUEST[$opt]) && $GLOBALS['timeout_passed']) {
232
            return htmlspecialchars($_REQUEST[$opt]);
233 20
        }
234 4
235
        if (! isset($GLOBALS['cfg'][$section][$opt])) {
236
            return '';
237 16
        }
238
239 16
        $matches = [];
240 12
        /* Possibly replace localised texts */
241
        if (! preg_match_all('/(str[A-Z][A-Za-z0-9]*)/', (string) $GLOBALS['cfg'][$section][$opt], $matches)) {
242
            return htmlspecialchars((string) $GLOBALS['cfg'][$section][$opt]);
243 4
        }
244 4
245 4
        $val = $GLOBALS['cfg'][$section][$opt];
246 4
        foreach ($matches[0] as $match) {
247
            if (! isset($GLOBALS[$match])) {
248
                continue;
249 4
            }
250
251
            $val = str_replace($match, $GLOBALS[$match], $val);
252 4
        }
253
254
        return htmlspecialchars($val);
255
    }
256
257
    /**
258
     * @param ExportPlugin[]|ImportPlugin[]|SchemaPlugin[] $list
259
     *
260
     * @return array<int, array<string, bool|string>>
261 4
     * @psalm-return list<array{name: non-empty-lowercase-string, text: string, is_selected: bool, force_file: bool}>
262
     */
263 4
    public static function getChoice(array $list, string $default): array
264 4
    {
265 4
        $return = [];
266 4
        foreach ($list as $plugin) {
267 4
            $pluginName = $plugin->getName();
268 1
            $properties = $plugin->getProperties();
269 4
            $return[] = [
270 4
                'name' => $pluginName,
271 4
                'text' => self::getString($properties->getText()),
272
                'is_selected' => $pluginName === $default,
273
                'force_file' => $properties->getForceFile(),
274
            ];
275 4
        }
276
277
        return $return;
278
    }
279
280
    /**
281
     * Returns single option in a list element
282
     *
283
     * @param string              $section       name of config section in $GLOBALS['cfg'][$section] for plugin
284
     * @param string              $plugin_name   unique plugin name
285
     * @param OptionsPropertyItem $propertyGroup options property main group instance
286
     * @param bool                $is_subgroup   if this group is a subgroup
287
     * @psalm-param 'Export'|'Import'|'Schema' $section
288
     *
289
     * @return string  table row with option
290
     */
291
    public static function getOneOption(
292
        $section,
293
        $plugin_name,
294
        &$propertyGroup,
295
        $is_subgroup = false
296
    ) {
297
        $ret = "\n";
298
299
        $properties = null;
300
        if (! $is_subgroup) {
301
            // for subgroup headers
302
            if (mb_strpos(get_class($propertyGroup), 'PropertyItem')) {
303
                $properties = [$propertyGroup];
304
            } else {
305
                // for main groups
306
                $ret .= '<div id="' . $plugin_name . '_' . $propertyGroup->getName() . '">';
307
308
                $text = null;
309
                if (method_exists($propertyGroup, 'getText')) {
310
                    $text = $propertyGroup->getText();
311
                }
312
313
                if ($text != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $text of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
314
                    $ret .= '<h5 class="card-title mt-4 mb-2">' . self::getString($text) . '</h5>';
315
                }
316
317
                $ret .= '<ul class="list-group">';
318
            }
319
        }
320
321
        $not_subgroup_header = false;
322
        if (! isset($properties)) {
323
            $not_subgroup_header = true;
324
            if (method_exists($propertyGroup, 'getProperties')) {
325
                $properties = $propertyGroup->getProperties();
326
            }
327
        }
328
329
        $property_class = null;
330
        if (isset($properties)) {
331
            /** @var OptionsPropertySubgroup $propertyItem */
332
            foreach ($properties as $propertyItem) {
333
                $property_class = get_class($propertyItem);
334
                // if the property is a subgroup, we deal with it recursively
335
                if (mb_strpos($property_class, 'Subgroup')) {
336
                    // for subgroups
337
                    // each subgroup can have a header, which may also be a form element
338
                    /** @var OptionsPropertyItem|null $subgroup_header */
339
                    $subgroup_header = $propertyItem->getSubgroupHeader();
340
                    if ($subgroup_header !== null) {
341
                        $ret .= self::getOneOption($section, $plugin_name, $subgroup_header);
342
                    }
343
344
                    $ret .= '<li class="list-group-item"><ul class="list-group"';
345
                    if ($subgroup_header !== null) {
346
                        $ret .= ' id="ul_' . $subgroup_header->getName() . '">';
347
                    } else {
348
                        $ret .= '>';
349
                    }
350
351
                    $ret .= self::getOneOption($section, $plugin_name, $propertyItem, true);
352
                    continue;
353
                }
354
355
                // single property item
356
                $ret .= self::getHtmlForProperty($section, $plugin_name, $propertyItem);
357
            }
358
        }
359
360
        if ($is_subgroup) {
361
            // end subgroup
362
            $ret .= '</ul></li>';
363
        } elseif ($not_subgroup_header) {
364
            // end main group
365
            $ret .= '</ul></div>';
366
        }
367
368
        if (method_exists($propertyGroup, 'getDoc')) {
369
            $doc = $propertyGroup->getDoc();
370
            if ($doc != null) {
371
                if (count($doc) === 3) {
372
                    $ret .= MySQLDocumentation::show($doc[1], false, null, null, $doc[2]);
373
                } elseif (count($doc) === 1) {
374
                    $ret .= MySQLDocumentation::showDocumentation('faq', $doc[0]);
375
                } else {
376
                    $ret .= MySQLDocumentation::show($doc[1]);
377
                }
378
            }
379
        }
380
381
        // Close the list element after $doc link is displayed
382
        if ($property_class !== null) {
383
            if (
384
                $property_class == BoolPropertyItem::class
385
                || $property_class == MessageOnlyPropertyItem::class
386
                || $property_class == SelectPropertyItem::class
387
                || $property_class == TextPropertyItem::class
388
            ) {
389
                $ret .= '</li>';
390
            }
391
        }
392
393
        return $ret . "\n";
394
    }
395
396
    /**
397
     * Get HTML for properties items
398
     *
399
     * @param string              $section      name of config section in
400
     *                                                            $GLOBALS['cfg'][$section] for plugin
401
     * @param string              $plugin_name  unique plugin name
402
     * @param OptionsPropertyItem $propertyItem Property item
403
     * @psalm-param 'Export'|'Import'|'Schema' $section
404
     *
405
     * @return string
406
     */
407
    public static function getHtmlForProperty(
408
        $section,
409
        $plugin_name,
410
        $propertyItem
411
    ) {
412
        $ret = '';
413
        $property_class = get_class($propertyItem);
414
        switch ($property_class) {
415
            case BoolPropertyItem::class:
416
                $ret .= '<li class="list-group-item">' . "\n";
417
                $ret .= '<div class="form-check form-switch">' . "\n";
418
                $ret .= '<input class="form-check-input" type="checkbox" role="switch" name="' . $plugin_name . '_'
419
                    . $propertyItem->getName() . '"'
420
                    . ' value="something" id="checkbox_' . $plugin_name . '_'
421
                    . $propertyItem->getName() . '"'
422
                    . ' '
423
                    . self::checkboxCheck(
424
                        $section,
425
                        $plugin_name . '_' . $propertyItem->getName()
426
                    );
427
428
                if ($propertyItem->getForce() != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $propertyItem->getForce() of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
429
                    // Same code is also few lines lower, update both if needed
430
                    $ret .= ' onclick="if (!this.checked &amp;&amp; '
431
                        . '(!document.getElementById(\'checkbox_' . $plugin_name
432
                        . '_' . $propertyItem->getForce() . '\') '
433
                        . '|| !document.getElementById(\'checkbox_'
434
                        . $plugin_name . '_' . $propertyItem->getForce()
435
                        . '\').checked)) '
436
                        . 'return false; else return true;"';
437
                }
438
439
                $ret .= '>';
440
                $ret .= '<label class="form-check-label" for="checkbox_' . $plugin_name . '_'
441
                    . $propertyItem->getName() . '">'
442
                    . self::getString($propertyItem->getText()) . '</label></div>';
443
                break;
444
            case DocPropertyItem::class:
445
                echo DocPropertyItem::class;
446
                break;
447
            case HiddenPropertyItem::class:
448
                $ret .= '<li class="list-group-item"><input type="hidden" name="' . $plugin_name . '_'
449
                    . $propertyItem->getName() . '"'
450
                    . ' value="' . self::getDefault(
451
                        $section,
452
                        $plugin_name . '_' . $propertyItem->getName()
453
                    )
454
                    . '"></li>';
455
                break;
456
            case MessageOnlyPropertyItem::class:
457
                $ret .= '<li class="list-group-item">' . "\n";
458
                $ret .= self::getString($propertyItem->getText());
459
                break;
460
            case RadioPropertyItem::class:
461
                /**
462
                 * @var RadioPropertyItem $pitem
463
                 */
464
                $pitem = $propertyItem;
465
466
                $default = self::getDefault(
467
                    $section,
468
                    $plugin_name . '_' . $pitem->getName()
469
                );
470
471
                $ret .= '<li class="list-group-item">';
472
473
                foreach ($pitem->getValues() as $key => $val) {
474
                    $ret .= '<div class="form-check"><input type="radio" name="' . $plugin_name
475
                        . '_' . $pitem->getName() . '" class="form-check-input" value="' . $key
476
                        . '" id="radio_' . $plugin_name . '_'
477
                        . $pitem->getName() . '_' . $key . '"';
478
                    if ($key == $default) {
479
                        $ret .= ' checked';
480
                    }
481
482
                    $ret .= '><label class="form-check-label" for="radio_' . $plugin_name . '_'
483
                        . $pitem->getName() . '_' . $key . '">'
484
                        . self::getString($val) . '</label></div>';
485
                }
486
487
                $ret .= '</li>';
488
489
                break;
490
            case SelectPropertyItem::class:
491
                /**
492
                 * @var SelectPropertyItem $pitem
493
                 */
494
                $pitem = $propertyItem;
495
                $ret .= '<li class="list-group-item">' . "\n";
496
                $ret .= '<label for="select_' . $plugin_name . '_'
497
                    . $pitem->getName() . '" class="form-label">'
498
                    . self::getString($pitem->getText()) . '</label>';
499
                $ret .= '<select class="form-select" name="' . $plugin_name . '_'
500
                    . $pitem->getName() . '"'
501
                    . ' id="select_' . $plugin_name . '_'
502
                    . $pitem->getName() . '">';
503
                $default = self::getDefault(
504
                    $section,
505
                    $plugin_name . '_' . $pitem->getName()
506
                );
507
                foreach ($pitem->getValues() as $key => $val) {
508
                    $ret .= '<option value="' . $key . '"';
509
                    if ($key == $default) {
510
                        $ret .= ' selected';
511
                    }
512
513
                    $ret .= '>' . self::getString($val) . '</option>';
514
                }
515
516
                $ret .= '</select>';
517
                break;
518
            case TextPropertyItem::class:
519
                /**
520
                 * @var TextPropertyItem $pitem
521
                 */
522
                $pitem = $propertyItem;
523
                $ret .= '<li class="list-group-item">' . "\n";
524
                $ret .= '<label for="text_' . $plugin_name . '_'
525
                    . $pitem->getName() . '" class="form-label">'
526
                    . self::getString($pitem->getText()) . '</label>';
527
                $ret .= '<input class="form-control" type="text" name="' . $plugin_name . '_'
528
                    . $pitem->getName() . '"'
529
                    . ' value="' . self::getDefault(
530
                        $section,
531
                        $plugin_name . '_' . $pitem->getName()
532
                    ) . '"'
533
                    . ' id="text_' . $plugin_name . '_'
534
                    . $pitem->getName() . '"'
535
                    . ($pitem->getSize() != null
536
                        ? ' size="' . $pitem->getSize() . '"'
537
                        : '')
538
                    . ($pitem->getLen() != null
539
                        ? ' maxlength="' . $pitem->getLen() . '"'
540
                        : '')
541
                    . '>';
542
                break;
543
            case NumberPropertyItem::class:
544
                $ret .= '<li class="list-group-item">' . "\n";
545
                $ret .= '<label for="number_' . $plugin_name . '_'
546
                    . $propertyItem->getName() . '" class="form-label">'
547
                    . self::getString($propertyItem->getText()) . '</label>';
548
                $ret .= '<input class="form-control" type="number" name="' . $plugin_name . '_'
549
                    . $propertyItem->getName() . '"'
550
                    . ' value="' . self::getDefault(
551
                        $section,
552
                        $plugin_name . '_' . $propertyItem->getName()
553
                    ) . '"'
554
                    . ' id="number_' . $plugin_name . '_'
555
                    . $propertyItem->getName() . '"'
556
                    . ' min="0"'
557
                    . '>';
558
                break;
559
            default:
560
                break;
561
        }
562
563
        return $ret;
564
    }
565
566
    /**
567
     * Returns html div with editable options for plugin
568
     *
569
     * @param string                                       $section name of config section in $GLOBALS['cfg'][$section]
570
     * @param ExportPlugin[]|ImportPlugin[]|SchemaPlugin[] $list    array with plugin instances
571
     * @psalm-param 'Export'|'Import'|'Schema' $section
572
     *
573
     * @return string  html fieldset with plugin options
574
     */
575
    public static function getOptions($section, array $list)
576
    {
577
        $ret = '';
578
        // Options for plugins that support them
579
        foreach ($list as $plugin) {
580
            $properties = $plugin->getProperties();
581
            $text = null;
582
            $options = null;
583
            if ($properties != null) {
584
                $text = $properties->getText();
585
                $options = $properties->getOptions();
586
            }
587
588
            $plugin_name = $plugin->getName();
589
590
            $ret .= '<div id="' . $plugin_name
591
                . '_options" class="format_specific_options">';
592
            $ret .= '<h3>' . self::getString($text) . '</h3>';
593
594
            $no_options = true;
595
            if ($options !== null && count($options) > 0) {
596
                foreach ($options->getProperties() as $propertyMainGroup) {
597
                    // check for hidden properties
598
                    $no_options = true;
599
                    foreach ($propertyMainGroup->getProperties() as $propertyItem) {
600
                        if (strcmp(HiddenPropertyItem::class, get_class($propertyItem))) {
601
                            $no_options = false;
602
                            break;
603
                        }
604
                    }
605
606
                    $ret .= self::getOneOption($section, $plugin_name, $propertyMainGroup);
607
                }
608
            }
609
610
            if ($no_options) {
611
                $ret .= '<p class="card-text">' . __('This format has no options') . '</p>';
612
            }
613
614
            $ret .= '</div>';
615
        }
616
617
        return $ret;
618
    }
619
620
    public static function getAuthPlugin(): AuthenticationPlugin
621
    {
622
        /** @psalm-var class-string $class */
623
        $class = 'PhpMyAdmin\\Plugins\\Auth\\Authentication'
624
            . ucfirst(strtolower($GLOBALS['cfg']['Server']['auth_type']));
625
626
        if (! class_exists($class)) {
627
            Core::fatalError(
628
                __('Invalid authentication method set in configuration:')
629
                . ' ' . $GLOBALS['cfg']['Server']['auth_type']
630
            );
631
        }
632
633
        /** @var AuthenticationPlugin $plugin */
634
        $plugin = new $class();
635
636
        return $plugin;
637
    }
638
}
639