Passed
Push — master ( 17bffe...10bee5 )
by Stefan
06:13
created

SimpleGUI::__construct()   D

Complexity

Conditions 15
Paths 96

Size

Total Lines 87
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 87
c 0
b 0
f 0
rs 4.9121
cc 15
eloc 58
nc 96
nop 0

How to fix   Long Method    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
 *******************************************************************************
4
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
5
 * and GN4-2 consortia
6
 *
7
 * License: see the web/copyright.php file in the file structure
8
 *******************************************************************************
9
 */
10
?>
11
<?php
12
// error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);
13
/**
14
 * This file contains the implementation of the simple CAT user interface
15
 * 
16
 * @author Tomasz Wolniewicz <[email protected]>
17
 * 
18
 * @package UserGUI
19
 * 
20
 */
21
22
$loggerInstance = new \core\common\Logging();
23
$loggerInstance->debug(4, "basic.php\n");
24
25
/**
26
 * SimpleGUI defines extensions of the GUI class used only in the simple interface
27
 * this class does not define its own constructor.
28
 */
29
class SimpleGUI extends \core\UserAPI {
30
    /**
31
     *  create the SimpleGUI object calling CAT constructor first
32
     *
33
     *  sets up all public prperties of the object
34
     */
35
    public function __construct() {
36
        parent::__construct();
37
        $validator = new \web\lib\common\InputValidation();
38
        $this->args = [];
39
        $this->page = 0;
40
        $this->languageInstance->setTextDomain('core');
41
        $this->args['lang'] = $this->languageInstance->getLang();
42
43
        /*
44
          The request may contain identifiers of country, idp, profile, device
45
          We require that if an identifiet of a lower level exists then all higher identifiers must also
46
          be present and match. If a mismatch occures that the lower level identifiers are dropped
47
         */
48
49
        if (isset($_REQUEST['reset_dev']) && $_REQUEST['reset_dev'] == 1) {
50
            unset($_REQUEST['device']);
51
        }
52
53
        /* Start with checking if we have the country code if not then use geolocation..
54
         */
55
        $federations = array_keys($this->printCountryList(1));
56
        if (isset($_REQUEST['country']) && $_REQUEST['country']) {
57
            $country = strtoupper($_REQUEST['country']);
58
        } else {
59
            $location = $this->locateUser();
60
            if ($location['status'] == 'ok') {
61
                $country = strtoupper($location['country']);
62
            } else {
63
                $this->loggerInstance->debug(2, "No coutry provided and unable to locate the address\n");
64
                $country = 'NONE';
65
            }
66
        }
67
        if (!in_array($country, $federations)) {
68
            $country = array_shift($federations);
69
        }
70
        $this->country = $validator->Federation($country);
71
        $this->args['country'] = $this->country->identifier;
72
        $this->page = 1;
73
74
// If we have IdP identifier then match country to this identifier
75
// if the request contians a country code and an IdP code that do nat match
76
// then drop the IdP code and just leave the country 
77
// If we have Profile identifier then test if we also have IdP identifier, if we do
78
// and they do not match then drop the profile code and just leave the IdP
79
80
        if (isset($_REQUEST['idp']) && $_REQUEST['idp']) {
81
            $this->page = 2;
82
            try {
83
                $this->idp = $validator->IdP($_REQUEST['idp']);
84
            } catch (Exception $fail) {
85
                $this->page = 1;
86
                $this->languageInstance->setTextDomain("web_user");
87
                return;
88
            }
89
            $countryTemp = new \core\Federation($this->idp->federation);
90
            if (strtoupper($this->country->identifier) !== strtoupper($countryTemp->identifier)) {
91
                unset($this->idp);
92
                $this->page = 1;
93
                $this->languageInstance->setTextDomain("web_user");
94
                return;
95
            }
96
            $this->args['idp'] = $this->idp->identifier;
97
            $this->profileCount = $this->idp->profileCount();
98
            if (!isset($_REQUEST['profile'])) {
99
                $this->languageInstance->setTextDomain("web_user");
100
                return;
101
            }
102
            $this->page = 3;
103
            try {
104
                $this->profile = $validator->Profile($_REQUEST['profile']);
105
            } catch (Exception $fail) {
106
                $this->page = 2;
107
                $this->languageInstance->setTextDomain("web_user");
108
                return;
109
            }
110
            if ($this->profile->institution != $this->idp->identifier) {
111
                unset($this->profile);
112
                $this->page = 2;
113
                $this->languageInstance->setTextDomain("web_user");
114
                return;
115
            }
116
            $this->args['profile'] = $this->profile->identifier;
117
            if (isset($_REQUEST['device'])) {
118
                $this->args['device'] = $validator->Device($_REQUEST['device']);
119
            }
120
        }
121
        $this->languageInstance->setTextDomain("web_user");
122
    }
123
124
// print country selection
125
    public function listCountries() {
126
        $out = '';
127
        $federations = $this->printCountryList(1);
128
        $out .= _('Select your country') . '<br>';
129
        $out .= '<select name="country" onchange="submit_form(this)">' . "\n";
130
        foreach ($federations as $fedId => $fedName) {
131
            $out .= '<option value="' . $fedId . '"';
132
            if ($fedId === $this->country->identifier) {
133
                $out .= ' selected';
134
            }
135
            $out .= '>' . $fedName . '</option>' . "\n";
136
        }
137
        $out .= '</select>';
138
        return $out;
139
    }
140
141
    public function listIdPs() {
142
        $instList = $this->orderIdentityProviders($this->country->identifier);
143
        $out = '';
144
        $out .= sprintf(_("Select your %s"), $this->nomenclature_inst );
145
        $out .= '<select name="idp" onchange="submit_form(this)">';
146
        if (!empty($instList)) {
147
            if (!isset($this->idp)) {
148
                $this->idp = new \core\IdP($instList[0]['idp']);
149
            }
150
            $idpId = $this->idp->identifier;
151
        }
152
        foreach ($instList as $oneInst) {
153
            $out .= '<option value="' . $oneInst['idp'] . '"';
154
            if ($oneInst['idp'] == $idpId) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $idpId does not seem to be defined for all execution paths leading up to this point.
Loading history...
155
                $out .= ' selected';
156
            }
157
            $out .= '>' . $oneInst['title'] . '</option>';
158
        }
159
        $out .= '</select>';
160
        return $out;
161
    }
162
163
    public function listProfiles() {
164
        if (empty($this->idp)) {
165
            return('');
166
        }
167
        $profiles = $this->idp->listProfiles(TRUE);
168
        if (!isset($this->profile)) {
169
            $this->profile = $profiles[0];
170
        }
171
        $profileId = $this->profile->identifier;
172
        $this->args['profile'] = $profileId;
173
        $out = '';
174
        if (count($profiles) > 1) {
175
            $out .= _("Select the user group") . '<br>';
176
            $out .= '<select name="profile" onchange="submit_form(this)">';
177
            foreach ($profiles as $profile) {
178
                $out .= '<option value="' . $profile->identifier . '"';
179
                if ($profile->identifier == $profileId) {
180
                    $out .= ' selected';
181
                }
182
                $out .= '>' . $profile->name . '</option>';
183
            }
184
            $out .= '</select>';
185
        } else {
186
            $out .= $this->passArgument('profile');
187
        }
188
        return $out;
189
    }
190
191
    public function listProfileDevices() {
192
        if (!isset($this->profile)) {
193
            return '';
194
        }
195
        $detectedOs = $this->detectOS();
196
        $deviceName = $detectedOs['device'];
197
        $this->args['device'] = $deviceName;
198
        $profileRedirect = 0;
199
        $redirectTarget = '';
200
        $deviceRedirects = '';
201
        $selectedOs = 0;
202
        $unsupportedMessage = '<div id="unsupported_os">' . sprintf(_("Your operating system was not properly detected, is not supported yet or cannot be configured with settings provided by your %s"), $this->nomenclature_inst) . "</div><br>";
203
204
        $attributes = $this->profileAttributes($this->profile->identifier);
0 ignored issues
show
Bug introduced by
It seems like $this->profile->identifier can also be of type string; however, parameter $profId of core\UserAPI::profileAttributes() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

204
        $attributes = $this->profileAttributes(/** @scrutinizer ignore-type */ $this->profile->identifier);
Loading history...
205
        $thedevices = $attributes['devices'];
206
        $message = '';
207
        if (!$deviceName) {
208
            $message = $unsupportedMessage;
209
        }
210
        if ($attributes['silverbullet']) {
211
            $out = _("You can download your eduroam installer via a personalised invitation link sent from your IT support. Please talk to the IT department to get this link.");
212
            return $out;
213
        }
214
        $out = _("Choose an installer to download") . '<br>';
215
        $out .= '<select name="device" onchange="set_device(this)">';
216
        $iterator = 0;
217
        foreach ($thedevices as $oneDevice) {
218 View Code Duplication
            if ((isset($oneDevice['options']) && isset($oneDevice['options']['hidden']) && $oneDevice['options']['hidden']) || $oneDevice['status']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
                continue;
220
            }
221
            if (!$deviceName) {
222
                $deviceName = $oneDevice['id'];
223
            }
224
            $disp = $oneDevice['display'];
225
            if ($oneDevice['id'] === '0') {
226
                $profileRedirect = 1;
227
                $redirectTarget = $oneDevice['redirect'];
228
            }
229
            $out .= '<option value="' . $oneDevice['id'] . '"';
230
            if ($oneDevice['id'] == $deviceName) {
231
                $out .= ' selected';
232
                $selectedOs = 1;
233
                if ($oneDevice['redirect']) {
234
                    $redirectTarget = $oneDevice['redirect'];
235
                }
236
            }
237
            $out .= '>' . $disp . '</option>';
238
            $deviceRedirects .= 'redirects[' . $iterator . '] = ' . ( $oneDevice['redirect'] ? 1 : 0 ) . ';';
239
            $iterator++;
240
        }
241
        $out .= '</select>';
242
        if ($selectedOs == 0) {
243
            $message = $unsupportedMessage;
244
        }
245
        $out = $message . $out;
246
        if ($profileRedirect) {
247
            $out = '';
248
        }
249
        if ($redirectTarget) {
250
            $deviceRedirects .= 'is_redirected = 1;';
251
            $out .= _("Your local administrator has specified a redirect to a local support page.") . '<br>' . _("When you click <b>CONTINUE</b> this support page will be opened.");
252
            $action = 'window.location.href=\'' . $redirectTarget . '\'; return(false);';
253
            $out .= "<p><button id='devices' name='devices' style='width:100%;' onclick=\"" . $action . '">' . _("CONTINUE to local support page") . "</button>";
254
        } else {
255
            $deviceRedirects .= 'is_redirected = 0;';
256
            $action = 'submit_form(this)';
257
            $out .= "<p><button id='devices' name='devices' style='width:100%;' onclick=\"" . $action . '">' . sprintf(_("Do you have an account at this %s?"), $this->nomenclature_inst) . '<br>' . _("If so and if the other settings above are OK then click here to download...") . "</button>";
258
        }
259
        $out .= '<script type="text/javascript">' . $deviceRedirects . '</script>';
260
        return $out;
261
    }
262
263
    public function displayDeviceDownload() {
264
        $this->languageInstance->setTextDomain('devices');
265
        $attributes = $this->profileAttributes($this->profile->identifier);
0 ignored issues
show
Bug introduced by
It seems like $this->profile->identifier can also be of type string; however, parameter $profId of core\UserAPI::profileAttributes() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

265
        $attributes = $this->profileAttributes(/** @scrutinizer ignore-type */ $this->profile->identifier);
Loading history...
266
        $thedevices = $attributes['devices'];
267
        $this->languageInstance->setTextDomain("web_user");
268
        $out = '';
269
        if (isset($attributes['description']) && $attributes['description']) {
270
            print '<div>' . $attributes['description'] . '</div>';
271
        }
272 View Code Duplication
        if (isset($attributes['local_email']) && $attributes['local_email']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
273
            $out .= '<p>Email: <a href="mailto:' . $attributes['local_email'] . '">' . $attributes['local_email'] . '</a>';
274
        }
275 View Code Duplication
        if (isset($attributes['local_url']) && $attributes['local_url']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
276
            $out .= '<p>WWW: <a href="' . $attributes['local_url'] . '">' . $attributes['local_url'] . '</a>';
277
        }
278 View Code Duplication
        if (isset($attributes['local_phone']) && $attributes['local_phone']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
            $out .= '<p>Tel: <a href="' . $attributes['local_phone'] . '">' . $attributes['local_phone'] . '</a>';
280
        }
281
        if ($out !== '') {
282
            print '<div class="user_info">';
283
            print sprintf(_("If you encounter problems you should ask for help at your %s"), $this->nomenclature_inst);
284
            print $out;
285
            print "</div>\n";
286
        }
287
288
        foreach ($thedevices as $oneDevice) {
289 View Code Duplication
            if (isset($oneDevice['options']) && isset($oneDevice['options']['hidden']) && $oneDevice['options']['hidden']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
290
                continue;
291
            }
292
            if ($oneDevice['id'] === '0') {
293
                print _("Your local administrator has specified a redirect to a local support page.") . ' ' . _("Click on the link below to continue.");
294
                print '<div style="width:100%; text-align:center"><a href ="' . $oneDevice['redirect'] . '">' . $oneDevice['redirect'] . '</a></div>';
295
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
296
            }
297
            if ($oneDevice['id'] === $this->args['device']) {
298
                break;
299
            }
300
        }
301
        $this->languageInstance->setTextDomain("web_user");
302
303
        $installer = $this->generateInstaller($this->args['device'], $this->profile->identifier);
0 ignored issues
show
Bug introduced by
It seems like $this->profile->identifier can also be of type string; however, parameter $profileId of core\UserAPI::generateInstaller() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

303
        $installer = $this->generateInstaller($this->args['device'], /** @scrutinizer ignore-type */ $this->profile->identifier);
Loading history...
304
        if (!$installer['link']) {
305
            print _("This is embarrassing. Generation of your installer failed. System admins have been notified. We will try to take care of the problem as soon as possible.");
306
            return;
307
        }
308
        $extraText = '';
309
        if (isset($oneDevice['message']) && $oneDevice['message']) {
310
            $extraText = $oneDevice['message'];
311
        }
312
        if (isset($oneDevice['device_customtext']) && $oneDevice['device_customtext']) {
313
            if ($extraText) {
314
                $extraText .= '<p>';
315
            }
316
            $extraText .= $oneDevice['device_customtext'];
317
        }
318
        if (isset($oneDevice['eap_customtext']) && $oneDevice['eap_customtext']) {
319
            if ($extraText) {
320
                $extraText .= '<p>';
321
            }
322
            $extraText .= $oneDevice['eap_customtext'];
323
        }
324
        if ($extraText) {
325
            $extraText .= '<p>';
326
        }
327
        print $extraText;
328
329
        $downloadLink = 'user/API.php?action=downloadInstaller&api_version=2&generatedfor=user&lang=' . $this->languageInstance->getLang() . '&device=' . $installer['device'] . '&profile=' . $installer['profile'];
330
331
        print '<p><button id="download_button" onclick="window.location.href=\'' . rtrim(dirname($_SERVER['SCRIPT_NAME']), '/') . '/' . $downloadLink . '\'; return(false)"><div>' . _("Download installer for") . '<br><span style="color:yellow; font-weight: bold">' . $oneDevice['display'] . '</span></div></button>';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $oneDevice seems to be defined by a foreach iteration on line 288. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
332
333
        print '<p><button id="start_over" name="start_over" onclick="submit_form(this)">' . _("Start over") . '</button>';
334
        print $this->passArgument('country');
335
        print $this->passArgument('idp');
336
        print $this->passArgument('profile');
337
        print $this->passArgument('device');
338
    }
339
340
    public function langSelection() {
341
        $out = _("View this page in") . " ";
342
        $out .= '<select onchange="submit_form(this)" name="lang">';
343
        foreach (CONFIG['LANGUAGES'] as $lang => $value) {
344
            $out .= '<option value="' . $lang . '"';
345
            if ($lang === $this->languageInstance->getLang()) {
346
                $out .= ' selected';
347
            }
348
            $out .= '>' . $value['display'] . '</option>';
349
        }
350
        $out .= '</select>';
351
        return $out;
352
    }
353
354
    /**
355
     * displays the navigation bar showing the current location of the page
356
     */
357
    public function yourChoice() {
358
        $out = '';
359
        $capitalisedCountry = strtoupper($this->country->identifier);
360
        $name = isset($this->knownFederations[$capitalisedCountry]) ? $this->knownFederations[$capitalisedCountry] : $capitalisedCountry;
361
        $name = preg_replace('/ +/', '&nbsp;', $name);
362
        $out .= "$name; ";
363
        $name = $this->idp->name;
364
        $name = preg_replace('/ +/', '&nbsp;', $name);
365
        $out .= "$name";
366
        if ($this->profileCount > 1) {
367
            $name = '; ' . $this->profile->name;
368
            $name = preg_replace('/ +/', '&nbsp;', $name);
369
            $out .= "$name";
370
        }
371
        return $out;
372
    }
373
374
    /**
375
     * generates a hidden input field with the given argName
376
     * 
377
     * @param string $argName name of the hidden input field
378
     * @return string
379
     */
380
    public function passArgument($argName) {
381
        return '<input type="hidden" name="' . $argName . '" value="' . $this->args[$argName] . '">';
382
    }
383
384
    public $country;
385
    public $idp;
386
    public $profile;
387
    public $args;
388
    public $profileCount;
389
    public $page;
390
391
}
392
393
$Gui = new SimpleGUI();
394
395
$loggerInstance->debug(4, "\n----------------------------------SIMPLE.PHP------------------------\n");
396
?>
397
<!DOCTYPE html>
398
<?php
399
$langObject = new \core\common\Language();
400
?>
401
<html xmlns="http://www.w3.org/1999/xhtml" lang="<?php echo $langObject->getLang() ?>">
402
    <head lang="<?php echo $langObject->getLang() ?>"> 
403
        <title><?php echo CONFIG['APPEARANCE']['productname_long']; ?></title>
404
        <link rel="stylesheet" media="screen" type="text/css" href="<?php echo $skinObject->findResourceUrl("CSS", "cat-basic.css.php"); ?>" />
405
        <meta charset="utf-8" /> 
406
        <script type="text/javascript">
407
            var redirects = new Array();
408
            var is_redirected = 0;
409
            function set_device(s) {
410
                if (redirects[s.selectedIndex] || is_redirected) {
411
                    my_form.submit();
412
                } else {
413
                    return;
414
                }
415
            }
416
            function submit_form(id) {
417
                if (id.name === 'country')
418
                    document.getElementById('reset_dev').value = 1;
419
                if (id.name === 'profile')
420
                    document.getElementById('reset_dev').value = 1;
421
                if (id.name === 'idp')
422
                    document.getElementById('reset_dev').value = 1;
423
                if (id.name === 'start_over')
424
                    document.getElementById('devices_h').value = 0;
425
                if (id.name === 'devices')
426
                    document.getElementById('devices_h').value = 1;
427
                my_form.submit();
428
            }
429
        </script>
430
    </head>
431
    <body style="">
432
        <?php print '<div id="motd">' . ( isset(CONFIG['APPEARANCE']['MOTD']) ? CONFIG['APPEARANCE']['MOTD'] : '&nbsp' ) . '</div>'; ?>
433
        <form name="my_form" method="POST" action="<?php echo $_SERVER['SCRIPT_NAME'] ?>" accept-charset='UTF-8'>
434
            <img src="<?php echo $skinObject->findResourceUrl("IMAGES", "consortium_logo.png"); ?>" style="width: 20%; padding-right:20px; padding-top:0px; float:right" alt="logo" />
435
            <?php
436
            /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
437
              if($Gui->page == 0) {
438
              print "<h1 style='color:red'>"._("no matching data found")."</h1>";
439
              $Gui->page = 2;
440
              }
441
             */
442
            $langObject = new \core\common\Language();
443
            print '<h1><a href="' . $_SERVER['SCRIPT_NAME'] . '?lang=' . $langObject->getLang() . '">' . CONFIG['APPEARANCE']['productname'] . '</a></h1>';
444
            print $Gui->langSelection();
445
            if (!isset($_REQUEST['devices_h']) || $_REQUEST['devices_h'] == 0 || isset($_REQUEST['start_over'])) {
446
                print "<p>\n";
447
                print $Gui->listCountries();
448
                if ($Gui->page == 2 && !isset($FED[strtoupper($Gui->country->identifier)])) {
449
                    $Gui->page = 1;
450
                }
451
                print "<p>" . $Gui->listIdPs();
452
                print "<p>" . $Gui->listProfiles();
453
                print "<p>" . $Gui->listProfileDevices();
454
                print '<input type="hidden" name="devices_h" id="devices_h" value="0">';
455
            } else {
456
                if ($Gui->page != 3) {
457
                    print "Arguments missmatch error.";
458
                    exit;
459
                }
460
                print '<div id="user_choice">' . $Gui->yourChoice() . '</div><p>';
461
                $Gui->displayDeviceDownload();
462
                print '<input type="hidden" name="devices_h" id="devices_h" value="1">';
463
            }
464
            ?>
465
            <input type="hidden" name="reset_dev" id="reset_dev" value="0">
466
        </form>
467
        <div class='footer'><hr />
468
            <?php
469
            print('<a href="tou.php">' . _("Terms of use") . "</a><p>");
470
            echo $Gui->CAT_COPYRIGHT;
471
            echo "</div>";
472
            ?>
473
    </body>
474
</html>
475