hotspotlogin 0.1.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +34 -13
- data/examples/etc/lighttpd/lighttpd.conf +31 -0
- data/examples/hotspotlogin.conf.yaml +6 -0
- data/lib/hotspotlogin/app.rb +74 -50
- data/lib/hotspotlogin/app/helpers.rb +17 -0
- data/lib/hotspotlogin/config.rb +25 -1
- data/lib/hotspotlogin/constants.rb +32 -2
- data/public/hotspotlogin/css/default.css +77 -0
- data/public/hotspotlogin/js/ChilliLibrary.js +844 -0
- data/public/hotspotlogin/js/UserStatus.js +229 -0
- data/views/_login_form.erb +5 -3
- data/views/hotspotlogin.erb +78 -18
- data/views/layout.erb +31 -43
- metadata +16 -11
@@ -0,0 +1,229 @@
|
|
1
|
+
// Requires ChilliLibrary.js
|
2
|
+
// See: http://www.coova.org/CoovaChilli/JSON
|
3
|
+
//
|
4
|
+
// Copyright(c) 2010, Guido De Rosa (guido.derosa at vemarsas.it) .
|
5
|
+
// License: MIT
|
6
|
+
|
7
|
+
chilliController.sessionTimeLeft = function() {
|
8
|
+
return Math.max(
|
9
|
+
(
|
10
|
+
chilliController.session.sessionTimeout -
|
11
|
+
chilliController.accounting.sessionTime
|
12
|
+
),
|
13
|
+
0
|
14
|
+
);
|
15
|
+
}
|
16
|
+
chilliController.idleTimeLeft = function() {
|
17
|
+
return Math.max(
|
18
|
+
(
|
19
|
+
chilliController.session.idleTimeout -
|
20
|
+
chilliController.accounting.idleTime
|
21
|
+
),
|
22
|
+
0
|
23
|
+
);
|
24
|
+
}
|
25
|
+
|
26
|
+
chilliController.scheduleSessionTimeoutAutorefresh = function() {
|
27
|
+
if (chilliController.sessionTimeLeft() && !chilliController.sessionTimeoutTimer) {
|
28
|
+
chilliController.sessionTimeoutTimer = {
|
29
|
+
preLogoff: setTimeout(
|
30
|
+
'chilliController.refresh()', 1000 * chilliController.sessionTimeLeft()
|
31
|
+
),
|
32
|
+
atLogoff: setTimeout( // 3 seconds delay looks fair
|
33
|
+
'chilliController.refresh()', 1000 * (3 + chilliController.sessionTimeLeft())
|
34
|
+
)
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
chilliController.scheduleIdleTimeoutAutorefresh = function() {
|
40
|
+
if (chilliController.idleTimeLeft()) {
|
41
|
+
if (chilliController.idleTimeoutTimer) {
|
42
|
+
clearTimeout(chilliController.idleTimeoutTimer.preLogoff);
|
43
|
+
clearTimeout(chilliController.idleTimeoutTimer.atLogoff);
|
44
|
+
}
|
45
|
+
chilliController.idleTimeoutTimer = {
|
46
|
+
preLogoff: setTimeout(
|
47
|
+
'chilliController.refresh()', 1000 * chilliController.idleTimeLeft()
|
48
|
+
),
|
49
|
+
atLogoff: setTimeout( // 3 seconds delay looks fair
|
50
|
+
'chilliController.refresh()', 1000 * (3 + chilliController.idleTimeLeft())
|
51
|
+
)
|
52
|
+
}
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
function showUserStatus(h) {
|
57
|
+
|
58
|
+
// Utility functions and objects
|
59
|
+
//
|
60
|
+
function formatStateCode(code) {
|
61
|
+
switch(code) {
|
62
|
+
case chilliController.stateCodes.UNKNOWN:
|
63
|
+
return 'Unknown';
|
64
|
+
case chilliController.stateCodes.NOT_AUTH:
|
65
|
+
return 'Not Authorized';
|
66
|
+
case chilliController.stateCodes.AUTH:
|
67
|
+
return 'Authorized';
|
68
|
+
case chilliController.stateCodes.AUTH_PENDING:
|
69
|
+
return 'Authorization Pending';
|
70
|
+
case chilliController.stateCodes.AUTH_SPLASH:
|
71
|
+
return 'AUTH_SPLASH'; // What does it mean?
|
72
|
+
default:
|
73
|
+
return code;
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
// CoovaChilli JSON interface only supports CHAP... don't use JSON to login.
|
78
|
+
function loginURL() {
|
79
|
+
var schema = chilliController.ssl ? 'https' : 'http';
|
80
|
+
return schema + '://' + h.uamip + ':' + h.uamport;
|
81
|
+
}
|
82
|
+
|
83
|
+
// chilliController.debug = true;
|
84
|
+
|
85
|
+
// If you use non standard configuration, define your configuration
|
86
|
+
if (h.uamip)
|
87
|
+
chilliController.host = h.uamip; // default: 192.168.182.1
|
88
|
+
if (h.uamport)
|
89
|
+
chilliController.port = h.uamport; // default: 3990
|
90
|
+
|
91
|
+
// We choose 5 minutes because is the default interval of Chilli->Radius
|
92
|
+
// accounting updates, and looks reasonable for busy sites (avoiding too
|
93
|
+
// much load on the network infrastructure and servers) .
|
94
|
+
chilliController.interval = (h.interval || 300); // default = 30
|
95
|
+
|
96
|
+
// then define event handler functions
|
97
|
+
chilliController.onError = handleErrors;
|
98
|
+
chilliController.onUpdate = updateUI ;
|
99
|
+
|
100
|
+
// get current state
|
101
|
+
chilliController.refresh() ;
|
102
|
+
|
103
|
+
initTable();
|
104
|
+
|
105
|
+
function initTable() {
|
106
|
+
// show them when/if we need them; but do not hide them when
|
107
|
+
// session terminates
|
108
|
+
document.getElementById('sessionTimeLeft:row').style.display = 'none';
|
109
|
+
document.getElementById('idleTimeout:row').style.display = 'none';
|
110
|
+
}
|
111
|
+
|
112
|
+
function updateHeadings(clientState) {
|
113
|
+
txt = null;
|
114
|
+
switch(clientState) {
|
115
|
+
case chilliController.stateCodes.NOT_AUTH:
|
116
|
+
txt = 'Logged out from HotSpot';
|
117
|
+
break;
|
118
|
+
case chilliController.stateCodes.AUTH:
|
119
|
+
txt = 'Logged in to HotSpot';
|
120
|
+
break;
|
121
|
+
}
|
122
|
+
if (txt) {
|
123
|
+
document.title = txt;
|
124
|
+
if (document.getElementById('headline'))
|
125
|
+
document.getElementById('headline').innerHTML = txt;
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
function updateLinks(clientState) {
|
130
|
+
var e = document.getElementById('logInLogOut');
|
131
|
+
if (e) {
|
132
|
+
switch(clientState) {
|
133
|
+
case chilliController.stateCodes.NOT_AUTH:
|
134
|
+
e.setAttribute('href', loginURL());
|
135
|
+
e.innerHTML = 'Login';
|
136
|
+
break;
|
137
|
+
case chilliController.stateCodes.AUTH:
|
138
|
+
e.setAttribute('href', '#');
|
139
|
+
e.onclick = chilliController.logoff;
|
140
|
+
e.innerHTML = 'Logout';
|
141
|
+
break;
|
142
|
+
}
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
// when the reply is ready, this handler function is called
|
147
|
+
function updateUI( cmd ) {
|
148
|
+
updateHeadings( chilliController.clientState);
|
149
|
+
updateLinks( chilliController.clientState);
|
150
|
+
var userName_e = document.getElementById('userName');
|
151
|
+
var clientState_e = document.getElementById('clientState');
|
152
|
+
var sessionTime_e = document.getElementById('sessionTime');
|
153
|
+
var sessionTimeLeft_e = document.getElementById('sessionTimeLeft');
|
154
|
+
var download_e = document.getElementById('download');
|
155
|
+
var upload_e = document.getElementById('upload');
|
156
|
+
var interval_e = document.getElementById('interval');
|
157
|
+
if (userName_e) {
|
158
|
+
userName_e.innerHTML = (
|
159
|
+
chilliController.session.userName
|
160
|
+
);
|
161
|
+
}
|
162
|
+
if (clientState_e) {
|
163
|
+
clientState_e.innerHTML = (
|
164
|
+
formatStateCode(chilliController.clientState)
|
165
|
+
);
|
166
|
+
}
|
167
|
+
//if (chilliController.terminateCause) {
|
168
|
+
// document.getElementById('terminateCause').innerHTML = (
|
169
|
+
// chilliController.terminateCause
|
170
|
+
// )
|
171
|
+
//}
|
172
|
+
if (sessionTime_e) {
|
173
|
+
document.getElementById('sessionTime').innerHTML = (
|
174
|
+
chilliController.formatTime(
|
175
|
+
chilliController.accounting.sessionTime, '0')
|
176
|
+
);
|
177
|
+
}
|
178
|
+
if (sessionTimeLeft_e) {
|
179
|
+
if (chilliController.session.sessionTimeout) {
|
180
|
+
document.getElementById('sessionTimeLeft').innerHTML = (
|
181
|
+
chilliController.formatTime(chilliController.sessionTimeLeft(), 0)
|
182
|
+
);
|
183
|
+
document.getElementById('sessionTimeLeft:row').style.display = '';
|
184
|
+
} else {
|
185
|
+
document.getElementById('sessionTimeLeft').innerHTML = ''
|
186
|
+
}
|
187
|
+
}
|
188
|
+
if (chilliController.session.idleTimeout) {
|
189
|
+
document.getElementById('idleTimeout').innerHTML = (
|
190
|
+
chilliController.formatTime(chilliController.accounting.idleTime) +
|
191
|
+
' / ' +
|
192
|
+
chilliController.formatTime(chilliController.session.idleTimeout)
|
193
|
+
);
|
194
|
+
document.getElementById('idleTimeout:row').style.display = '';
|
195
|
+
}
|
196
|
+
var download_bytes =
|
197
|
+
chilliController.accounting.inputOctets +
|
198
|
+
Math.pow(2, 32) * chilliController.accounting.inputGigawords;
|
199
|
+
var upload_bytes =
|
200
|
+
chilliController.accounting.outputOctets +
|
201
|
+
Math.pow(2, 32) * chilliController.accounting.outputGigawords;
|
202
|
+
if (download_e) {
|
203
|
+
download_e.innerHTML = (
|
204
|
+
chilliController.formatBytes(download_bytes, 0)
|
205
|
+
);
|
206
|
+
}
|
207
|
+
if (upload_e) {
|
208
|
+
upload_e.innerHTML = (
|
209
|
+
chilliController.formatBytes(upload_bytes, 0)
|
210
|
+
);
|
211
|
+
}
|
212
|
+
if (interval_e) {
|
213
|
+
interval_e.innerHTML = (
|
214
|
+
chilliController.formatTime(chilliController.interval, 0)
|
215
|
+
);
|
216
|
+
}
|
217
|
+
|
218
|
+
chilliController.scheduleSessionTimeoutAutorefresh();
|
219
|
+
chilliController.scheduleIdleTimeoutAutorefresh();
|
220
|
+
}
|
221
|
+
|
222
|
+
// If an error occurs, this handler will be called instead
|
223
|
+
function handleErrors ( code ) {
|
224
|
+
alert('The last contact with the Controller failed. Error code =' + code);
|
225
|
+
}
|
226
|
+
|
227
|
+
|
228
|
+
}
|
229
|
+
|
data/views/_login_form.erb
CHANGED
@@ -6,14 +6,16 @@
|
|
6
6
|
<table>
|
7
7
|
<tbody>
|
8
8
|
<tr>
|
9
|
-
<
|
9
|
+
<th scope="row">Login:</td>
|
10
10
|
<td><input type="text" name="UserName" size="20" maxlength="255"></td>
|
11
11
|
</tr>
|
12
12
|
<tr>
|
13
|
-
<
|
13
|
+
<th scope="row">Password:</td>
|
14
14
|
<td><input type="password" name="Password" size="20" maxlength="255"></td>
|
15
15
|
</tr>
|
16
16
|
</tbody>
|
17
17
|
</table>
|
18
|
-
<
|
18
|
+
<div id="submit-container">
|
19
|
+
<input type="submit" name="login" value="login">
|
20
|
+
</div>
|
19
21
|
</form>
|
data/views/hotspotlogin.erb
CHANGED
@@ -1,22 +1,36 @@
|
|
1
|
-
|
1
|
+
<%
|
2
|
+
require 'hotspotlogin/app/helpers'
|
3
|
+
%>
|
4
|
+
|
5
|
+
<% if custom_headline %>
|
6
|
+
<h1><%= custom_headline %></h1>
|
7
|
+
<% end %>
|
8
|
+
<% if logoext %>
|
9
|
+
<div id="logo-container"><img src="/hotspotlogin/logo<%= logoext %>"/></div>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<h2 id="headline"><%= titel %></h2>
|
13
|
+
|
14
|
+
<% if custom_text and File.file? custom_text %>
|
15
|
+
<div id="custom-text"><%= File.read custom_text %></div>
|
16
|
+
<% end %>
|
17
|
+
|
18
|
+
<%# TODO: use constants.rb %>
|
2
19
|
<% if [1, 4, 12].include? result %>
|
3
|
-
<div
|
4
|
-
<a
|
5
|
-
|
6
|
-
|
20
|
+
<div id="logInLogOut-container">
|
21
|
+
<a
|
22
|
+
id="logInLogOut"
|
23
|
+
href="http://<%= uamip %>:<%= uamport %>/logoff">Logout</a>
|
24
|
+
<a href="#" onClick="javascript:chilliController.refresh();">Refresh</a>
|
7
25
|
</div>
|
8
|
-
<% elsif [2, 5].include? result %>
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
<div style="text-align: center;">
|
17
|
-
<a href="http://<%= uamip %>:<%= uamport %>/prelogin">
|
18
|
-
Login
|
19
|
-
</a>
|
26
|
+
<% elsif [2, 5, 3, 13].include? result %>
|
27
|
+
<div id="form-container">
|
28
|
+
<%=
|
29
|
+
erb(
|
30
|
+
:_login_form,
|
31
|
+
:layout => false
|
32
|
+
)
|
33
|
+
%>
|
20
34
|
</div>
|
21
35
|
<% elsif result == 11 %>
|
22
36
|
<p>Please wait.. </p> <!-- popup1 -->
|
@@ -24,7 +38,53 @@
|
|
24
38
|
<p>Login must be performed through ChilliSpot daemon!</p>
|
25
39
|
<% end %>
|
26
40
|
|
27
|
-
|
28
41
|
<% if params['reply'] %>
|
29
42
|
<div style="text-align: center;"><%= params['reply'] %></div>
|
30
43
|
<% end %>
|
44
|
+
|
45
|
+
<% if status_window?(result) %>
|
46
|
+
<div id="status-container">
|
47
|
+
<table id="status-table">
|
48
|
+
<tbody>
|
49
|
+
<tr id="userName:row">
|
50
|
+
<th scope="row">Username</th>
|
51
|
+
<td id="userName"></td>
|
52
|
+
<tr id="clientState:row">
|
53
|
+
<th scope="row">Client State</th>
|
54
|
+
<td id="clientState"></td>
|
55
|
+
</tr>
|
56
|
+
<tr id="sessionTime:row">
|
57
|
+
<th scope="row">Session Time</th>
|
58
|
+
<td id="sessionTime"></td>
|
59
|
+
</tr>
|
60
|
+
<tr id="sessionTimeLeft:row">
|
61
|
+
<th scope="row">Session Time Left</th>
|
62
|
+
<td id="sessionTimeLeft"></th>
|
63
|
+
</tr>
|
64
|
+
<tr id="idleTimeout:row">
|
65
|
+
<th scope="row">Idle Time/Timeout</th>
|
66
|
+
<td id="idleTimeout"></th>
|
67
|
+
</tr>
|
68
|
+
<tr id="download:row">
|
69
|
+
<th scope="row">Download Traffic</th>
|
70
|
+
<td id="download"></td>
|
71
|
+
</tr>
|
72
|
+
<tr id="upload:row">
|
73
|
+
<th scope="row">Upload Traffic</th>
|
74
|
+
<td id="upload"></td>
|
75
|
+
</tr>
|
76
|
+
<tr id="interval:row">
|
77
|
+
<th scope="row" class="optinfo">Automatically updated every</th>
|
78
|
+
<td id="interval"></td>
|
79
|
+
</tr>
|
80
|
+
</tbody>
|
81
|
+
</table>
|
82
|
+
</div>
|
83
|
+
|
84
|
+
<% end %>
|
85
|
+
|
86
|
+
<% if custom_footer and File.file? custom_footer %>
|
87
|
+
<div id="custom-footer"><%= File.read custom_footer %></div>
|
88
|
+
<% end %>
|
89
|
+
|
90
|
+
|
data/views/layout.erb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
<%
|
2
|
+
require 'hotspotlogin/constants'
|
3
|
+
|
2
4
|
loginpath = request.path_info
|
3
5
|
%>
|
4
6
|
|
@@ -8,47 +10,17 @@
|
|
8
10
|
<title><%= titel %></title>
|
9
11
|
<meta http-equiv="Cache-control" content="no-cache">
|
10
12
|
<meta http-equiv="Pragma" content="no-cache">
|
11
|
-
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
12
13
|
|
13
|
-
<script language="JavaScript">
|
14
|
+
<script language="JavaScript"> // legacy stuff from hotspotlogin.cgi/php
|
15
|
+
var width = 500; // popup
|
16
|
+
var height = 500; // popup
|
14
17
|
var blur = 0;
|
15
18
|
var starttime = new Date();
|
16
19
|
var startclock = starttime.getTime();
|
17
20
|
var mytimeleft = 0;
|
18
|
-
|
19
|
-
function doTime() {
|
20
|
-
window.setTimeout( "doTime()", 1000 );
|
21
|
-
t = new Date();
|
22
|
-
time = Math.round((t.getTime() - starttime.getTime())/1000);
|
23
|
-
if (mytimeleft) {
|
24
|
-
time = mytimeleft - time;
|
25
|
-
if (time <= 0) {
|
26
|
-
window.location = "<%= loginpath %>?res=popup3&uamip=<%= uamip %>&uamport=<%= uamport %>";
|
27
|
-
}
|
28
|
-
}
|
29
|
-
if (time < 0) time = 0;
|
30
|
-
hours = (time - (time % 3600)) / 3600;
|
31
|
-
time = time - (hours * 3600);
|
32
|
-
mins = (time - (time % 60)) / 60;
|
33
|
-
secs = time - (mins * 60);
|
34
|
-
if (hours < 10) hours = "0" + hours;
|
35
|
-
if (mins < 10) mins = "0" + mins;
|
36
|
-
if (secs < 10) secs = "0" + secs;
|
37
|
-
title = "Online time: " + hours + ":" + mins + ":" + secs;
|
38
|
-
if (mytimeleft) {
|
39
|
-
title = "Remaining time: " + hours + ":" + mins + ":" + secs;
|
40
|
-
}
|
41
|
-
if(document.all || document.getElementById){
|
42
|
-
document.title = title;
|
43
|
-
}
|
44
|
-
else {
|
45
|
-
self.status = title;
|
46
|
-
}
|
47
|
-
}
|
48
|
-
|
49
21
|
function popUp(URL) {
|
50
22
|
if (self.name != "chillispot_popup") {
|
51
|
-
chillispot_popup = window.open(URL, 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=
|
23
|
+
chillispot_popup = window.open(URL, 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=' + width + ',height=' + height);
|
52
24
|
}
|
53
25
|
}
|
54
26
|
|
@@ -56,21 +28,17 @@
|
|
56
28
|
if (timeleft) {
|
57
29
|
mytimeleft = timeleft;
|
58
30
|
}
|
59
|
-
if ((result == 1) && (self.name == "chillispot_popup")) {
|
60
|
-
doTime();
|
61
|
-
}
|
62
31
|
if ((result == 1) && (self.name != "chillispot_popup")) {
|
63
|
-
chillispot_popup = window.open(URL, 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=
|
32
|
+
chillispot_popup = window.open(URL, 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=' + width + ',height=' + height);
|
64
33
|
}
|
65
34
|
if ((result == 2) || result == 5) {
|
66
35
|
document.form1.UserName.focus()
|
67
36
|
}
|
68
37
|
if ((result == 2) && (self.name != "chillispot_popup")) {
|
69
|
-
chillispot_popup = window.open('', 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width=
|
38
|
+
chillispot_popup = window.open('', 'chillispot_popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=1,width='+ width + ',height=' + height);
|
70
39
|
chillispot_popup.close();
|
71
40
|
}
|
72
41
|
if ((result == 12) && (self.name == "chillispot_popup")) {
|
73
|
-
doTime();
|
74
42
|
if (redirurl) {
|
75
43
|
opener.location = redirurl;
|
76
44
|
}
|
@@ -98,15 +66,35 @@
|
|
98
66
|
}
|
99
67
|
}
|
100
68
|
</script>
|
69
|
+
|
70
|
+
<link rel="stylesheet" href="/hotspotlogin/css/default.css"/>
|
71
|
+
<link rel="shortcut icon" href="/hotspotlogin/favicon.ico"/>
|
72
|
+
|
101
73
|
</head>
|
102
74
|
|
103
|
-
<body onLoad="javascript:doOnLoad(<%= result %>, '<%= request.path_info %>?res=popup2&uamip=<%= uamip %>&uamport=<%= uamport %>&userurl=<%= userurl %>&redirurl=<%= redirurl %>&timeleft=<%= timeleft %>','<%= userurl %>', '<%= redirurl %>', '<%= timeleft %>')" onBlur="javascript:doOnBlur(
|
104
|
-
<div
|
105
|
-
hotspotlogin.rb
|
75
|
+
<body onLoad="javascript:doOnLoad(<%= result %>, '<%= request.path_info %>?res=popup2&uamip=<%= uamip %>&uamport=<%= uamport %>&userurl=<%= userurl %>&redirurl=<%= redirurl %>&timeleft=<%= timeleft %>','<%= userurl %>', '<%= redirurl %>', '<%= timeleft %>')" onBlur="javascript:doOnBlur(<%= result %>)">
|
76
|
+
<div id="powered-by">
|
77
|
+
Powered by <a href="http://rubygems.org/gems/hotspotlogin">hotspotlogin.rb</a>
|
106
78
|
</div>
|
107
79
|
<div id="main">
|
108
80
|
<%= yield %>
|
109
81
|
</div>
|
82
|
+
<% if status_window?(result) %>
|
83
|
+
<!-- CovaChilli JSON interface -->
|
84
|
+
<script
|
85
|
+
type="text/javascript" src="hotspotlogin/js/ChilliLibrary.js">
|
86
|
+
</script>
|
87
|
+
<script
|
88
|
+
type="text/javascript" src="hotspotlogin/js/UserStatus.js">
|
89
|
+
</script>
|
90
|
+
<script language="JavaScript">
|
91
|
+
showUserStatus( {
|
92
|
+
uamip: "<%= uamip %>",
|
93
|
+
uamport: <%= uamport %>,
|
94
|
+
interval: <%= interval %>
|
95
|
+
} );
|
96
|
+
</script>
|
97
|
+
<% end %>
|
110
98
|
</body>
|
111
99
|
</html>
|
112
100
|
|