]> bicyclesonthemoon.info Git - ott/bsta/blob - 2words.1.pl
input validation; goto form; show version; 2 words password
[ott/bsta] / 2words.1.pl
1 ###RUN_PERL: #!/usr/bin/perl
2
3 # /bsta/2words
4 # 2words is generated from 2words.1.pl.
5 #
6 # The wordgame interface
7 #
8 # Copyright (C) 2016, 2017, 2023, 2024  Balthasar SzczepaƄski
9 #
10 # This program is free software: you can redistribute it and/or modify
11 # it under the terms of the GNU Affero General Public License as
12 # published by the Free Software Foundation, either version 3 of the
13 # License, or (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU Affero General Public License for more details.
19 #
20 # You should have received a copy of the GNU Affero General Public License
21 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
23 use strict;
24 use utf8;
25 # use Encode::Locale ('decode_argv');
26 use Encode ('encode', 'decode');
27
28 ###PERL_LIB: use lib /botm/lib/bsta
29 use botm_common (
30         'HTTP_STATUS',
31         'http_header_status', 'http_header_allow',
32         'merge_url',
33         'read_header_env',
34         'html_entity_encode_dec',
35         'url_query_decode', 'url_query_encode',
36         'open_encoded'
37 );
38 use bsta_lib (
39         'STATE', 'INTF_STATE',
40         'get_id',
41         'fail_method', 'fail_content_type',
42         'print_html_start', 'print_html_end',
43         'print_html_head_start', 'print_html_head_end',
44         'print_html_body_start', 'print_html_body_end',
45         'write_index',
46         'get_remote_addr', 'get_password',
47         'merge_settings',
48         'ong',
49         'read_story', 'write_story',
50         'read_settings', 'read_state'
51 );
52
53 ###PERL_CGI_PATH:           CGI_PATH           = /bsta/
54 ###PERL_CGI_2WORDS_PATH:    CGI_2WORDS_PATH    = /bsta/2words
55
56 ###PERL_DATA_STORY_PATH:    DATA_STORY_PATH    = /botm/data/bsta/story
57
58 ###PERL_WEBSITE_NAME:       WEBSITE_NAME       = Bicycles on the Moon
59
60 ###PERL_STORY_LENGTH:       STORY_LENGTH       = 16
61 ###PERL_PAGE_LENGTH:        PAGE_LENGTH        = 16
62 ###PERL_FIRSTPAGE_LENGTH:   FIRSTPAGE_LENGTH   = 4
63
64 binmode STDIN,  ':encoding(UTF-8)';
65 binmode STDOUT, ':encoding(UTF-8)';
66 binmode STDERR, ':encoding(UTF-8)';
67 # decode_argv();
68
69 my %http;
70 my %cgi;
71 my %story;
72 my %new_story;
73 my %settings;
74 my %state;
75
76 my $time = time();
77 srand ($time-$$);
78
79 my $method;
80 my $IP;
81 my $words;
82 my $color2;
83 my $last_IP;
84 my $story_id;
85 my $turn;
86 my $status;
87 my $allow;
88 my $message;
89 my $first_letter;
90 my $second_letter;
91 my $last_letter;
92 my $intf_state;
93 my $intf_pass;
94 my $intf_pause;
95 my $intf_mode;
96 my $fh;
97 my $story_lock;
98 my @story_lines;
99 my $ong_state;
100 my $page;
101 my $password;
102 my $password_ok;
103
104
105 delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
106 ###PERL_SET_PATH: $ENV{'PATH'} = /usr/local/bin:/usr/bin:/bin;
107
108 if ($ENV{'REQUEST_METHOD'} =~ /^(HEAD|GET|POST)$/) {
109         $method = $1;
110 }
111 else {
112         exit fail_method($ENV{'REQUEST_METHOD'}, ['GET','POST', 'HEAD']);
113 }
114
115 %http = read_header_env(\%ENV);
116 %cgi = url_query_decode($ENV{'QUERY_STRING'});
117
118 if ($method eq 'POST') {
119         if ($http{'content-type'} eq 'application/x-www-form-urlencoded') {
120                 my %cgi_post = url_query_decode( <STDIN> );
121                 %cgi = merge_settings(\%cgi, \%cgi_post);
122         }
123         # multipart not supported
124         else{
125                 exit fail_content_type($method, $http{'content-type'});
126         }
127 }
128
129 $IP = get_remote_addr();
130 $page = get_id(\%cgi);
131 $password = get_password(\%cgi);
132 if ($cgi{'words'} ne '') {
133         $words = $cgi{'words'};
134 }
135
136 %settings = read_settings();
137 %state    = read_state();
138 $ong_state = int($state{'state'});
139
140 $password_ok = ($password eq $settings{'password'});
141 if ($password_ok) {
142         $IP .= ' OK';
143 }
144
145 $story_lock=0;
146 if (open_encoded($fh, "+<:encoding(UTF-8)", DATA_STORY_PATH())) {
147         $story_lock=1;
148         if (flock($fh,2)) {
149                 $story_lock=2;
150         }
151         %story = read_story($fh);
152         
153         if ($story{'lastip'} =~ /^.+$/) {
154                 $last_IP=$&;
155         }
156         else {
157                 $last_IP='0.0.0.0';
158         }
159         
160         $last_letter = lc($story{'letter'});
161         $story_id   = int($story{'id'});
162         $intf_pass  = int($story{'pass'});
163         $intf_state = int($story{'state'});
164         $intf_mode  = $intf_state & INTF_STATE->{'mode'};
165         $intf_pause = $intf_state & INTF_STATE->{'||'};
166         
167         if ($IP ne $last_IP) {
168                 $turn = 1;
169         }
170         else {
171                 $turn = 0;
172         }
173         
174         if (
175                 ($intf_state < 0) || (
176                         ($method eq 'POST') && (
177                                 ($cgi{'clear'} ne '') || 
178                                 ($cgi{'clear_all'} ne '')
179                         )
180                 )
181         ) {
182                 if (
183                         ($cgi{'clear_all'} ne '') ||
184                         ($intf_state < -1)
185                 ) {
186                         $story{'id'} = 0;
187                 }
188                 $story{'content'} = '';
189                 $story{'lastip' } = '0.0.0.0';
190                 $story{'letter' } = '';
191                 $story{'pass'   } = 0;
192                 $story{'state'  } = INTF_STATE->{'X'};
193                 $turn = 0;
194                 if ($ong_state == STATE->{'inactive'}) {
195                         write_index(
196                                 \%state,
197                                 \%settings,
198                                 $story{'pass'},
199                                 $story{'state'},
200                                 0 # pause
201                         );
202                 }
203                 write_story($fh, \%story);
204         }
205         
206         if (($words ne '') && ($method eq 'POST')) {
207                 if (
208                         (!$turn) &&
209                         (!$password_ok)
210                 ) {
211                         $status = HTTP_STATUS->{'forbidden'};
212                         $message = "It's not your turn.";
213                 }
214                 # TODO: consider allowing non-ASCII letters in words.
215                 # (not very important in English language)
216                 elsif (
217                         ($words =~ /^([!"\(\),\.:;\?][ \t]*)?([A-Za-z][A-Za-z'\-]*[A-Za-z']?)([!"\(\),\.:;\? \t][ \t]*)([A-Za-z][A-Za-z'\-]*[A-Za-z']?)([!"\(\),\.:;\?]?[ \t]*)$/) ||
218                         ($password_ok && ($words ne ''))
219                 ) {
220                         # we have 2 words
221                         $first_letter  = lc(substr($2, 0, 1));
222                         $second_letter = lc(substr($4, 0, 1));
223                         if (
224                                 ($first_letter ne $last_letter) &&
225                                 ($last_letter ne '') && 
226                                 (!$password_ok)
227                         ) {
228                                 $status = HTTP_STATUS->{'bad_request'};
229                                 $message = 'The first word must start with '.uc($last_letter).'.';
230                         }
231                         elsif (
232                                 ($first_letter eq $second_letter) &&
233                                 (!$password_ok)
234                         ) {
235                                 $status = HTTP_STATUS->{'bad_request'};
236                                 $message = 'The second word can\'t also start with '.uc($first_letter).'.';
237                         }
238                         else {
239                                 # words are valid
240                                 # update state
241                                 $story{'content'} = $story{'content'} . $words."\n";
242                                 $turn = 0;
243                                 $story{'lastip'} = $IP;
244                                 $story{'letter'} = $second_letter;
245                                 
246                                 if ($cgi{'next'} ne '') {
247                                         # start next game
248                                         if (
249                                                 $password_ok ||
250                                                 (split(/\r?\n/,$story{'content'}) >= (STORY_LENGTH-1))
251                                         ) {
252                                                 # store finished game
253                                                 write_story($story_id, \%story);
254                                                 # init new game
255                                                 $new_story{'id'     } = $story_id + 1;
256                                                 $new_story{'letter' } = '';
257                                                 $new_story{'lastip' } = $IP;
258                                                 $new_story{'content'} = '';
259                                                 $new_story{'pass'   } = 0;
260                                                 $new_story{'state'  } = INTF_STATE->{'X'};
261                                                 # reset hidden interface
262                                                 $intf_state = INTF_STATE->{'X'};
263                                                 $intf_pass = 0;
264                                                 $intf_mode = INTF_STATE->{'X'};
265                                                 $intf_pause= 0;
266                                                 if($ong_state == STATE->{'inactive'}) {
267                                                         # ONG not activated yet; reset index
268                                                         write_index(
269                                                                 \%state,
270                                                                 \%settings,
271                                                                 $intf_pass,
272                                                                 $intf_mode,
273                                                                 $intf_pause
274                                                         );
275                                                 }
276                                                 # save new game
277                                                 write_story($fh, \%new_story);
278                                         }
279                                         else {
280                                                 $message = 'To early to finish this wordgame.';
281                                                 write_story($fh, \%story);
282                                         }
283                                 }
284                                 else {
285                                         # continue the game
286                                         if ($intf_pass == 1) {
287                                                 # hidden interface was already active; deactivate
288                                                 $intf_pass = 2;
289                                                 $story{'pass'} = 2;
290                                                 if($ong_state == STATE->{'inactive'}) {
291                                                         write_index(
292                                                                 \%state,
293                                                                 \%settings,
294                                                                 $intf_pass,
295                                                                 $intf_mode,
296                                                                 $intf_pause
297                                                         );
298                                                 }
299                                         }
300                                         elsif(lc($2).' '.lc($4) eq $settings{'unlock'}) {
301                                                 # correct password for the hidden interface!
302                                                 if ($intf_pass != 0) {
303                                                         $message = 'The password has already been used in this story.';
304                                                 }
305                                                 elsif ($ong_state != STATE->{'inactive'}) {
306                                                         # ONG already active, nothing to do here
307                                                         $message = "???";
308                                                 }
309                                                 else {
310                                                         # ready to activate?
311                                                         my $r;
312                                                         
313                                                         # ONG tape interface
314                                                         $r = ong(
315                                                                 'i',   # ID: tape interface
316                                                                 $time, # ONG time;    not relevant
317                                                                 0,     # timer;       not relevant
318                                                                 0,     # update;      not relevant
319                                                                 0,     # print
320                                                                 \%settings,         # not relevant
321                                                                 '',    # %default;    not relevant
322                                                                 '',    # %frame_data; not relevant
323                                                                 ''     # $goto_list;  not relevant
324                                                         );
325                                                         if ($r) {
326                                                                 # ONG CFRT
327                                                                 $r = ong(
328                                                                         'c',   # ID: CFRT
329                                                                         $time, # ONG time;   not relevant
330                                                                         0,     # timer;      not relevant
331                                                                         0,     # update;     not relevant
332                                                                         0,     # print
333                                                                         \%settings,
334                                                                         '',    # %default
335                                                                         '',    # %frame_data
336                                                                         ''     # $goto_list; not relevant
337                                                                 );
338                                                         }
339                                                         if ($r) {
340                                                                 # ONG frame 0!
341                                                                 $r = ong(
342                                                                         0,     # frame ID
343                                                                         $time, # ONG time; might get overwritten later
344                                                                         0,     # timer
345                                                                         0,     # update
346                                                                         0,     # print
347                                                                         \%settings,
348                                                                         '',    # %default
349                                                                         '',    # %frame_data
350                                                                         ''     # $goto_list
351                                                                 );
352                                                         }
353                                                         if($r) {
354                                                                 # new state of hidden interface
355                                                                 $intf_pass = 1;
356                                                                 $intf_state = INTF_STATE->{'X'};
357                                                                 $intf_mode  = INTF_STATE->{'X'};
358                                                                 $intf_pause = 0;
359                                                                 $story{'pass'} = 1;
360                                                                 $story{'state'} = INTF_STATE->{'X'};
361                                                                 write_index(
362                                                                         \%state,
363                                                                         \%settings,
364                                                                         $intf_pass,
365                                                                         $intf_mode,
366                                                                         $intf_pause
367                                                                 );
368                                                         }
369                                                 }
370                                         }
371                                         write_story($fh, \%story);
372                                 }
373                         }
374                 }
375                 else {
376                         $status = HTTP_STATUS->{'bad_request'};
377                         $message = 'Please, two words, not more, not less (some punctuation is allowed).';
378                 }
379         }
380         elsif (
381                 ($cgi{'s'} ne '') &&
382                 ($intf_pass == 1) &&
383                 ($ong_state == STATE->{'inactive'})
384         ) {
385                 $intf_state = int($cgi{'s'}) & INTF_STATE->{'mask'};
386                 $intf_mode  = $intf_state & INTF_STATE->{'mode'};
387                 $intf_pause = $intf_state & INTF_STATE->{'||'};
388                 $story{'state'} = $intf_state;
389                 write_index(
390                         \%state,
391                         \%settings,
392                         $intf_pass,
393                         $intf_mode,
394                         $intf_pause
395                 );
396                 write_story($fh, \%story);
397         }
398         @story_lines = split(/\r?\n/, $story{'content'});
399         if(@story_lines & 1) {
400                 $turn = !$turn;
401         }
402         
403         close($fh);
404 }
405
406 if ($status ne '') {
407         print http_header_status($status);
408 }
409 if ($allow ne '') {
410         print http_header_allow($allow);
411 }
412 print "Content-type: text/html; charset=UTF-8\n\n";
413
414 if($method eq 'HEAD') {
415         exit;
416 }
417
418 my $max_page = int(($story_id + PAGE_LENGTH - FIRSTPAGE_LENGTH - 1) / PAGE_LENGTH);
419 my $newer_available = ($page > 0);
420 my $older_available = ($page < $max_page);
421 my $show_intf = ($intf_pass == 1) && ($ong_state == STATE->{'inactive'});
422 my $id_start = 
423         $story_id-1 -(
424                 ($page == 0)    ? 0 : (
425                         (($page-1) * PAGE_LENGTH ) + FIRSTPAGE_LENGTH
426                 )
427         );
428 my $id_stop = $story_id-1 - (($page*PAGE_LENGTH) + FIRSTPAGE_LENGTH);
429 if ($id_stop < 0) {
430         $id_stop = -1;
431 }
432
433 my $bsta_url = CGI_PATH;
434 my $twowords_url = CGI_2WORDS_PATH;
435 my $newer_url;
436 my $older_url;
437 my $oldest_url;
438 my $newest_url = merge_url(
439         {'path' => $twowords_url},
440         {'path' => 0}
441 );
442 if ($newer_available) {
443         $newer_url = merge_url(
444                 {'path' => $twowords_url},
445                 {'path' => $page-1}
446         );
447 }
448 if ($older_available) {
449         $older_url = merge_url(
450                 {'path' => $twowords_url},
451                 {'path' => $page+1}
452         );
453         $oldest_url = merge_url(
454                 {'path' => $twowords_url},
455                 {'path' => $max_page}
456         );
457 }
458 my $button_4_url = merge_url(
459         {'path' => $twowords_url},
460         {'query' => {'s' => (INTF_STATE->{'>'} | $intf_pause)}}
461 );
462 my $button_3_url = merge_url(
463         {'path' => $twowords_url},
464         {'query' => {'s' => (INTF_STATE->{'<<'} | $intf_pause)}}
465 );
466 my $button_2_url = merge_url(
467         {'path' => $twowords_url},
468         {'query' => {'s' => (INTF_STATE->{'>>'} | $intf_pause)}}
469 );
470 my $button_1_url = merge_url(
471         {'path' => $twowords_url},
472         {'query' => {'s' => INTF_STATE->{'X'}}}
473 );
474 my $button_0_url = merge_url(
475         {'path' => $twowords_url},
476         {'query' => {'s' => ($intf_pause ? $intf_mode : ($intf_mode | INTF_STATE->{'||'}))}}
477 );
478 my $button_5_img = merge_url(
479         {'path' => CGI_PATH()},
480         {'path' => 'intf-20.gif'}
481 );
482 my $button_4_img = merge_url(
483         {'path' => CGI_PATH()},
484         {'path' => 'intf-10'.(($intf_mode == INTF_STATE->{'>'}) ? '_' : '').'.gif'}
485 );
486 my $button_3_img = merge_url(
487         {'path' => CGI_PATH()},
488         {'path' => 'intf-08'.(($intf_mode == INTF_STATE->{'<<'}) ? '_' : '').'.gif'}
489 );
490 my $button_2_img = merge_url(
491         {'path' => CGI_PATH()},
492         {'path' => 'intf-04'.(($intf_mode == INTF_STATE->{'>>'}) ? '_' : '').'.gif'}
493 );
494 my $button_1_img = merge_url(
495         {'path' => CGI_PATH()},
496         {'path' => 'intf-02.gif'}
497 );
498 my $button_0_img = merge_url(
499         {'path' => CGI_PATH()},
500         {'path' => 'intf-01'.($intf_pause ? '_' : '').'.gif'}
501 );
502 my $intf_img_id = '';
503 if ($intf_state == INTF_STATE->{'>'}) {
504         $intf_img_id = '_10'
505 }
506 elsif ($intf_mode == INTF_STATE->{'<<'}) {
507         $intf_img_id = '_08'
508 }
509 elsif ($intf_mode == INTF_STATE->{'>>'}) {
510         $intf_img_id = '_04'
511 }
512 my $intf_img = merge_url(
513         {'path' => CGI_PATH()},
514         {'path' => 'intf-00'.$intf_img_id.'.gif'}
515 );
516
517 if ($password_ok) {
518         my $password_query = url_query_encode({'p', $settings{'password'}});
519         $twowords_url = merge_url($twowords_url , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
520         $newest_url   = merge_url($newest_url   , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
521         $newer_url    = merge_url($newer_url    , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
522         $older_url    = merge_url($older_url    , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
523         $button_4_url = merge_url($button_4_url , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
524         $button_3_url = merge_url($button_3_url , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
525         $button_2_url = merge_url($button_2_url , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
526         $button_1_url = merge_url($button_1_url , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
527         $button_0_url = merge_url($button_0_url , {'query' => $password_query, 'append_query' => 1, 'preserve_fragment' => 1});
528 }
529
530 my $_password = $password_ok ? html_entity_encode_dec($settings{'password'}, 1): '';
531 my $_bsta_url     = html_entity_encode_dec($bsta_url     , 1);
532 my $_twowords_url = html_entity_encode_dec($twowords_url , 1);
533 my $_newest_url   = html_entity_encode_dec($newest_url   , 1);
534 my $_newer_url    = html_entity_encode_dec($newer_url    , 1);
535 my $_older_url    = html_entity_encode_dec($older_url    , 1);
536 my $_oldest_url   = html_entity_encode_dec($oldest_url   , 1);
537 my $_button_4_url = html_entity_encode_dec($button_4_url , 1);
538 my $_button_3_url = html_entity_encode_dec($button_3_url , 1);
539 my $_button_2_url = html_entity_encode_dec($button_2_url , 1);
540 my $_button_1_url = html_entity_encode_dec($button_1_url , 1);
541 my $_button_0_url = html_entity_encode_dec($button_0_url , 1);
542 my $_button_5_img = html_entity_encode_dec($button_5_img , 1);
543 my $_button_4_img = html_entity_encode_dec($button_4_img , 1);
544 my $_button_3_img = html_entity_encode_dec($button_3_img , 1);
545 my $_button_2_img = html_entity_encode_dec($button_2_img , 1);
546 my $_button_1_img = html_entity_encode_dec($button_1_img , 1);
547 my $_button_0_img = html_entity_encode_dec($button_0_img , 1);
548 my $_intf_img     = html_entity_encode_dec($intf_img     , 1);
549 my $_message      = html_entity_encode_dec($message      , 1);
550 my $_website_name = html_entity_encode_dec(WEBSITE_NAME(), 1);
551
552 print_html_start(\*STDOUT);
553 print_html_head_start(\*STDOUT);
554
555
556 print '  <title>Two words &bull; '.$_website_name.'</title>'."\n";
557 print '  <link rel="start" href="'.$_oldest_url.'">'."\n";
558 if ($older_available) {
559         print '  <link rel="prev" href="'.$_older_url.'">'."\n";
560 }
561 if ($newer_available) {
562         print '  <link rel="next" href="'.$_newer_url.'">'."\n";
563 }
564
565 print_html_head_end(\*STDOUT);
566 print_html_body_start(\*STDOUT);
567
568 print '   <div id="inst" class="ins">'."\n";
569
570 print '    <div id="title">'."\n";
571 print '     <h1 id="titletext">Two words</h1>'."\n";
572 print '    </div>'."\n";
573
574 if ($page == 0) {
575         print '    <div id="storypuzzle">'."\n";
576         for (my $i = 0; $i < @story_lines; ++$i) {
577                 print '     <span class="'.($turn ? 'ni':'br').'">'.html_entity_encode_dec($story_lines[$i], 1).'</span>'."\n";
578                 $turn = !$turn;
579         }
580         print '    </div>'."\n";
581
582         print '    <div id="command">'."\n";
583         if ($message ne '') {
584                 print '     <span class="br">'.$_message.'</span>'."\n";
585         }
586         
587         if ($turn || $password_ok) {
588                 print '     <form method="post" action="'.$_twowords_url.'">'."\n";
589                 if ($message eq '') {
590                         if ($story{"content"} eq '') {
591                                 print '      Two words, please:<br>'."\n";
592                         }
593                         else {
594                                 print '      Please continue, two words:<br>'."\n";
595                         }
596                 }
597                 print '      <input class="intx" type="text" name="words">'."\n";
598                 print '      <input class="inbt" type="submit" value="enter">'."\n";
599                 if ((@story_lines >= (STORY_LENGTH-1)) || $password_ok ) {
600                         print '      <input class="inbt" type="submit" name="next" value="enter and then start a new one">'."\n";
601                 }
602                 if ($password_ok) {
603                         print '      <input class="inbt" type="submit" name="clear" value="clear">'."\n";
604                         print '      <input class="inbt" type="submit" name="clear_all" value="clear all">'."\n";
605                         print '      <input type="hidden" name="p" value="'.$_password.'">'."\n";
606                 }
607                 print '     </form>'."\n";
608         }
609         else {
610                 if ($message eq '') {
611                         print '     Wait for it.'."\n";
612                 }
613         }
614         print '    </div>'."\n";
615 }
616 elsif ($message ne '') {
617         print '    <div id="command">'."\n";
618         print '     <span class="br">'.$_message.'</span>'."\n";
619         print '    </div>'."\n";
620 }
621 print '   </div>'."\n";
622
623 if ($show_intf) {
624         print '   <div id="framespace">'."\n";
625         print '    <table id="intftable" cellspacing="0" cellpadding="0">'."\n";
626         print '     <tr class="intf">'."\n";
627         print '      <td colspan="6" class="intf"><img src="'.$_intf_img.'" alt="" class="intf"></td>'."\n";
628         print '     </tr>'."\n";
629         
630         print '     <tr class="intf">'."\n";
631         print '      <td class="intf"><img src="'.$_button_5_img.'" alt="o" class="intf"></td>'."\n";
632         print '      <td class="intf"><a href="'.$_button_4_url.'"><img src="'.$_button_4_img.'" class="intf" alt="&gt;"></a></td>'."\n";
633         print '      <td class="intf"><a href="'.$_button_3_url.'"><img src="'.$_button_3_img.'" class="intf" alt="&lt;&lt;"></a></td>'."\n";
634         print '      <td class="intf"><a href="'.$_button_2_url.'"><img src="'.$_button_2_img.'" class="intf" alt="&gt;&gt;"></a></td>'."\n";
635         print '      <td class="intf"><a href="'.$_button_1_url.'"><img src="'.$_button_1_img.'" class="intf" alt="^"></a></td>'."\n";
636         print '      <td class="intf"><a href="'.$_button_0_url.'"><img src="'.$_button_0_img.'" class="intf" alt="||"></a></td>'."\n";
637         print '     </tr>'."\n";
638         print '    </table>'."\n";
639         print '   </div>'."\n";
640 }
641
642 print '   <div id="insb" class="ins">'."\n";
643
644 print '    <div id="undertext">'."\n";
645 for (my $i = $id_start; $i > $id_stop; --$i) {
646         %new_story = read_story($i);
647         print '     <p class="'.(($i&1)?'br':'ni').'" id="s'.$i.'">'.html_entity_encode_dec($new_story{'content'}).'</p>'."\n";
648 }
649 print '    </div>'."\n";
650
651 print '    <div id="underlinks">'."\n";
652 print '     <a href="'.$_bsta_url.'">BSTA</a> |'."\n";
653 print '     <a href="'.$_twowords_url.'">Once again</a>';
654 if ($older_available) {
655         print ' |'."\n";
656         print '     <a href="'.$_older_url.'">Before</a>';
657 }
658 if ($newer_available) {
659         print ' |'."\n";
660         print '     <a href="'.$newer_url.'">Unbefore</a>';
661 }
662 if ($older_available) {
663         print ' |'."\n";
664         print '<a href="'.$_oldest_url.'">Initially</a>';
665 }
666 if($turn) {
667         print ' |'."\n";
668         print '     (Entering words here is irreversible. Your actions might be remembered forever. So please be reasonable.)';
669 }
670 print "\n";
671 print '    </div>'."\n";
672
673 print '   </div>'."\n";
674
675 print_html_body_end(\*STDOUT, $ong_state == STATE->{'inactive'});
676 print_html_end(\*STDOUT);