############################################################################### # System.pm # # $Date: 01.09.2026 $ # ############################################################################### # YaBB: Yet another Bulletin Board # # Open-Source Community Software for Webmasters # # Version: YaBBForum 3.1 # # Packaged: 01 Sep, 2026 # # Distributed by: https://yabbforum.nz # # =========================================================================== # # Copyright (c) 2000-2026 YaBB (yabbforum.nz) - All Rights Reserved. # # Software by: The YaBB Development Team # # with assistance from the YaBB community. # ############################################################################### use CGI::Carp qw(fatalsToBrowser); our $VERSION = '3.1'; $systempmver = 'YaBBForum 3.1'; sub BoardTotals { my ( $job, @updateboards ) = @_; my ( $line, @lines, $updateboard, @boardvars, $cnt ); if ( !@updateboards ) { @updateboards = @allboards; } chomp @updateboards; if (@updateboards) { my @tags = qw(board threadcount messagecount lastposttime lastposter lastpostid lastreply lastsubject lasticon lasttopicstate); if ( $job eq 'load' ) { fopen( FORUMTOTALS, "$boardsdir/forum.totals" ) or fatal_error( 'cannot_open', "$boardsdir/forum.totals", 1 ); @lines = ; fclose(FORUMTOTALS); chomp @lines; foreach $updateboard (@updateboards) { foreach $line (@lines) { @boardvars = split /\|/xsm, $line; if ( $boardvars[0] eq $updateboard && exists $board{ $boardvars[0] } ) { for my $cnt ( 1 .. $#tags ) { ${ $uid . $updateboard }{ $tags[$cnt] } = $boardvars[$cnt]; } last; } } } } elsif ( $job eq 'update' ) { fopen( FORUMTOTALS, "<$boardsdir/forum.totals" ) or fatal_error( 'cannot_open', "$boardsdir/forum.totals", 1 ); @lines = ; fclose( FORUMTOTALS ); for $line ( 0 .. ( $#lines ) ) { @boardvars = split /\|/xsm, $lines[$line]; if ( exists $board{ $boardvars[0] } ) { if ( $boardvars[0] eq $updateboards[0] ) { $lines[$line] = "$updateboards[0]|"; chomp $boardvars[9]; for my $cnt ( 1 .. $#tags ) { if ( exists( ${ $uid . $boardvars[0] }{ $tags[$cnt] } ) ) { $lines[$line] .= ${ $uid . $boardvars[0] }{ $tags[$cnt] }; } else { $lines[$line] .= $boardvars[$cnt]; } $lines[$line] .= $cnt < $#tags ? q{|} : "\n"; } } } else { $lines[$line] = q{}; } } fopen( FORUMTOTALS, ">$boardsdir/forum.totals" ) or fatal_error( 'cannot_open', "$boardsdir/forum.totals", 1 ); print {FORUMTOTALS} @lines or croak "$croak{'print'} FORUMTOTALS"; fclose(FORUMTOTALS); } elsif ( $job eq 'delete' ) { fopen( FORUMTOTALS, "<$boardsdir/forum.totals" ) or fatal_error( 'cannot_open', "$boardsdir/forum.totals", 1 ); @lines = ; fclose( FORUMTOTALS ); for my $line ( 0 .. $#lines ) { @boardvars = split /\|/xsm, $lines[$line], 2; if ( $boardvars[0] eq $updateboards[0] || !exists $board{ $boardvars[0] } ) { $lines[$line] = q{}; } } fopen( FORUMTOTALS, ">$boardsdir/forum.totals" ) or fatal_error( 'cannot_open', "$boardsdir/forum.totals", 1 ); print {FORUMTOTALS} @lines or croak "$croak{'print'} FORUMTOTALS"; fclose(FORUMTOTALS); } elsif ( $job eq 'add' ) { fopen( FORUMTOTALS, ">>$boardsdir/forum.totals" ) or fatal_error( 'cannot_open', "$boardsdir/forum.totals", 1 ); foreach (@updateboards) { print {FORUMTOTALS} "$_|0|0|N/A|N/A||||\n" or croak "$croak{'print'} FORUMTOTALS"; } fclose(FORUMTOTALS); } } return; } sub BoardCountTotals { my ($cntboard) = @_; if ( !$cntboard ) { return; } fopen( BOARD, "$boardsdir/$cntboard.txt" ) or fatal_error( 'cannot_open', "$boardsdir/$cntboard.txt", 1 ); my @threads = ; fclose(BOARD); my $threadcount = @threads; my $messagecount = $threadcount; for my $i ( 0 .. $#threads ) { my @threadline = split /\|/xsm, $threads[$i]; if ( $threadline[8] =~ /m/sm ) { $threadcount--; $messagecount--; next; } $messagecount += $threadline[5]; } ${ $uid . $cntboard }{'threadcount'} = $threadcount; ${ $uid . $cntboard }{'messagecount'} = $messagecount; BoardSetLastInfo( $cntboard, \@threads ); return; } sub BoardSetLastInfo { my ( $setboard, $board_ref ) = @_; my ( $lastthread, $lastthreadid, $lastthreadstate, @lastthreadmessages, @lastmessage ); foreach my $lastthread ( @{$board_ref} ) { if ($lastthread) { ( $lastthreadid, undef, undef, undef, undef, undef, undef, undef, $lastthreadstate ) = split /\|/xsm, $lastthread; if ( $lastthreadstate !~ /m/sm ) { chomp $lastthreadstate; fopen( FILE, "$datadir/$lastthreadid.txt" ) or fatal_error( 'cannot_open', "$datadir/$lastthreadid.txt", 1 ); @lastthreadmessages = ; fclose(FILE); @lastmessage = split /\|/xsm, $lastthreadmessages[-1], 7; last; } $lastthreadid = q{}; } } ${ $uid . $setboard }{'lastposttime'} = $lastthreadid ? $lastmessage[3] : 'N/A'; ${ $uid . $setboard }{'lastposter'} = $lastthreadid ? ( $lastmessage[4] eq 'Guest' ? "Guest-$lastmessage[1]" : $lastmessage[4] ) : 'N/A'; ${ $uid . $setboard }{'lastpostid'} = $lastthreadid ? $lastthreadid : q{}; ${ $uid . $setboard }{'lastreply'} = $lastthreadid ? $#lastthreadmessages : q{}; ${ $uid . $setboard }{'lastsubject'} = $lastthreadid ? $lastmessage[0] : q{}; ${ $uid . $setboard }{'lasticon'} = $lastthreadid ? $lastmessage[5] : q{}; ${ $uid . $setboard }{'lasttopicstate'} = ( $lastthreadid && $lastthreadstate ) ? $lastthreadstate : '0'; BoardTotals( 'update', $setboard ); return; } #### THREAD MANAGEMENT #### sub MessageTotals { # usage: &MessageTotals("task",) # tasks: update, load, incview, incpost, decpost, recover my ( $job, $updatethread ) = @_; chomp $updatethread; if ( !$updatethread ) { return; } if ( $job eq 'update' ) { if ( ${$updatethread}{'board'} eq q{} ) { ## load if the variable is not already filled MessageTotals( 'load', $updatethread ); } } elsif ( $job eq 'load' ) { if ( ${$updatethread}{'board'} ne q{} ) { return; } ## skip load if the variable is already filled fopen( CTB, "$datadir/$updatethread.ctb", 1 ); while ( my $inp = ) { if ( $inp =~ /^'(.*?)',"(.*?)"/xsm ) { ${$updatethread}{$1} = $2; } } fclose(CTB); @repliers = split /,/xsm, ${$updatethread}{'repliers'}; return; } elsif ( $job eq 'incview' ) { ${$updatethread}{'views'}++; } elsif ( $job eq 'incpost' ) { ${$updatethread}{'replies'}++; } elsif ( $job eq 'decpost' ) { ${$updatethread}{'replies'}--; } elsif ($job eq 'inclike') { @tmplike = split /,/xsm, ${$updatethread}{'like'}; push @tmplike, $username; ${$updatethread}{'like'} = join ',', @tmplike; } elsif ($job eq 'declike') { @likes = split /,/xsm, ${$updatethread}{'like'}; $likenum = 0; foreach(@likes) { if($_ eq $username) { splice(@likes, $likenum, 1); last; } $likenum++; } ${$updatethread}{'like'} = join ',', @likes; } elsif ( $job eq 'recover' ) { # storing thread status my $threadstatus; my $openboard = ${$updatethread}{'board'}; fopen( TESTBOARD, "$boardsdir/$openboard.txt" ) or fatal_error( 'cannot_open', "$boardsdir/$openboard.txt", 1 ); while ( $ThreadLine = ) { if ( $updatethread == ( split /\|/xsm, $ThreadLine, 2 )[0] ) { $threadstatus = ( split /\|/xsm, $ThreadLine )[8]; chomp $threadstatus; last; } } fclose(TESTBOARD); # storing thread other info fopen( MSG, "$datadir/$updatethread.txt" ) or fatal_error( 'cannot_open', "$datadir/$updatethread.txt", 1 ); my @threaddata = ; fclose(MSG); my @lastinfo = split /\|/xsm, $threaddata[-1]; my $lastpostdate = sprintf '%010d', $lastinfo[3]; my $lastposter = $lastinfo[4] eq 'Guest' ? qq~Guest-$lastinfo[1]~ : $lastinfo[4]; # rewrite/create a correct thread.ctb ${$updatethread}{'replies'} = $#threaddata; ${$updatethread}{'views'} = ${$updatethread}{'views'} || 0; ${$updatethread}{'lastposter'} = $lastposter; ${$updatethread}{'lastpostdate'} = $lastpostdate; ${$updatethread}{'threadstatus'} = $threadstatus; ${$updatethread}{'like'} = ${$updatethread}{'like'}; @repliers = (); } else { return; } ## trap writing false ctb files on forged num= actions ## if ( -e "$datadir/$updatethread.txt" ) { my $newtime = ctbtime(); ${$updatethread}{'repliers'} = join q{,}, @repliers; # Changes here on @tag must also be done in Post.pm -> sub Post2 -> my @tag = ... my @tag = qw(board replies views lastposter lastpostdate threadstatus repliers); push @tag, 'like'; fopen( UPDATE_CTB, ">$datadir/$updatethread.ctb", 1 ) or fatal_error( 'cannot_open', "$datadir/$updatethread.ctb", 1 ); print {UPDATE_CTB} qq~### ThreadID: $updatethread, LastModified: $newtime ###\n\n~ or croak "$croak{'print'} UPDATE_CTB"; for my $cnt ( 0 .. $#tag ) { print {UPDATE_CTB} qq~'$tag[$cnt]',"${$updatethread}{$tag[$cnt]}"\n~ or croak "$croak{'print'} UPDATE_CTB"; } fclose(UPDATE_CTB); } return; } #### USER AND MEMBERSHIP MANAGEMENT #### sub UserAccount { my ( $user, $action, $pars ) = @_; return if !${ $uid . $user }{'password'}; if ( $action eq 'update' ) { if ($pars) { foreach ( split /\+/xsm, $pars ) { ${ $uid . $user }{$_} = $date; } } elsif ( $username eq $user ) { ${ $uid . $user }{'lastonline'} = $date; } $userext = 'vars'; if ( !exists( ${ $uid . $user }{'reversetopic'} ) ) { ${ $uid . $user }{'reversetopic'} = $ttsreverse; } } elsif ( $action eq 'preregister' ) { $userext = 'pre'; } elsif ( $action eq 'register' ) { $userext = 'vars'; } elsif ( $action eq 'delete' ) { unlink "$memberdir/$user.vars"; return; } else { $userext = 'vars'; } # using sequential tag writing as hashes do not sort the way we like them to my @tags = qw(realname password position addgroups email hidemail regdate regtime regreason location bday hideage disableage gender disablegender userpic usertext signature template language stealth webtitle weburl skype myspace facebook twitter youtube msn gtalk timeselect user_tz dynamic_clock postcount lastonline lastpost lastim im_ignorelist im_popup im_imspop pmviewMess notify_me board_notifications thread_notifications favorites buddylist cathide pageindex reversetopic postlayout sesquest sesanswer session lastips onlinealert offlinestatus awaysubj awayreply awayreplysent spamcount spamtime hide_avatars hide_user_text hide_img hide_attach_img hide_signat hide_smilies_row numberformat collapsebdrules return_to); if ($extendedprofiles) { require Sources::ExtendedProfiles; push @tags, ext_get_fields_array(); } push @tags, 'topicpreview', 'collapsescpoll'; push @tags, 'hidedirectmessage'; #### Edit for Who is Where #### ## Mod hook ## fopen( UPDATEUSER, ">$memberdir/$user.$userext", 1 ) or fatal_error( 'cannot_open', "$memberdir/$user.$userext", 1 ); print {UPDATEUSER} "### User variables for ID: $user ###\n\n" or croak "$croak{'print'} UPDATEUSER"; for my $cnt ( 0 .. $#tags ) { print {UPDATEUSER} qq~'$tags[$cnt]',"${$uid.$user}{$tags[$cnt]}"\n~ or croak "$croak{'print'} UPDATEUSER"; } fclose(UPDATEUSER); return; } sub MemberIndex { my ( $memaction, $user, $mychk ) = @_; if ( $memaction eq 'add' && LoadUser($user) ) { $theregdate = stringtotime( ${ $uid . $user }{'regdate'} ); $theregdate = sprintf '%010d', $theregdate; if ( !${ $uid . $user }{'postcount'} ) { ${ $uid . $user }{'postcount'} = 0; } if ( !${ $uid . $user }{'position'} ) { ${ $uid . $user }{'position'} = MemberPostGroup( ${ $uid . $user }{'postcount'} ); } ManageMemberlist( 'add', $user, $theregdate ); ManageMemberinfo( 'add', $user, ${ $uid . $user }{'realname'}, ${ $uid . $user }{'email'}, ${ $uid . $user }{'position'}, ${ $uid . $user }{'postcount'} ); fopen( TTL, "$memberdir/members.ttl" ) or fatal_error( 'cannot_open', "$memberdir/members.ttl", 1 ); $buffer = ; fclose(TTL); ( $membershiptotal, undef ) = split /\|/xsm, $buffer; $membershiptotal++; fopen( TTL, ">$memberdir/members.ttl" ) or fatal_error( 'cannot_open', "$memberdir/members.ttl", 1 ); print {TTL} qq~$membershiptotal|$user~ or croak "$croak{'print'} TTL"; fclose(TTL); return 0; } elsif ( $memaction eq 'remove' && $user ) { ManageMemberlist( 'delete', $user ); ManageMemberinfo( 'delete', $user ); require Sources::Notify; removeNotifications($user); fopen( MEMLIST, "$memberdir/memberlist.txt" ) or fatal_error( 'cannot_open', "$memberdir/memberlist.txt", 1 ); @memberlt = ; fclose(MEMLIST); my $membershiptotal = @memberlt; my ( $lastuser, undef ) = split /\t/xsm, $memberlt[-1], 2; fopen( TTL, ">$memberdir/members.ttl" ) or fatal_error( 'cannot_open', "$memberdir/members.ttl", 1 ); print {TTL} qq~$membershiptotal|$lastuser~ or croak "$croak{'print'} TTL"; fclose(TTL); return 0; } elsif ( ( $memaction eq 'check_exist' || $memaction eq 'who_is' ) && $user ) { ManageMemberinfo('load'); while ( ( $curmemb, $value ) = each %memberinf ) { ( $curname, $curmail, $curposition, $curpostcnt ) = split /\|/xsm, $value; if ( $memaction eq 'check_exist') { if ( lc $user eq lc $curmemb && $mychk == 0 ) { undef %memberinf; return $curmemb; } elsif ( lc $user eq lc $curmail && $mychk == 2 ) { undef %memberinf; return $curmail; } elsif ( lc $user eq lc $curname && $mychk == 1 ) { undef %memberinf; return $curname; } } elsif ( $memaction eq 'who_is' && ( lc $user eq lc $curmemb || lc $user eq lc $curmail || ($screenlogin && lc $user eq lc $curname ) ) ) { undef %memberinf; return $curmemb; } } } # return; } sub MemberPostGroup { my ($userpostcnt) = @_; $grtitle = q{}; foreach my $postamount ( reverse sort { $a <=> $b } keys %Post ) { if ( $userpostcnt >= $postamount ) { ( $grtitle, undef ) = split /\|/xsm, $Post{$postamount}, 2; last; } } return $grtitle; } sub MembershipCountTotal { fopen( MEMBERLISTREAD, "$memberdir/memberlist.txt" ) or fatal_error( 'cannot_open', "$memberdir/memberlist.txt", 1 ); my @num = ; fclose(MEMBERLISTREAD); ( $latestmember, $meminfo ) = split /\t/xsm, $num[-1]; my $membertotal = @num; undef @num; fopen( MEMTTL, ">$memberdir/members.ttl" ) or fatal_error( 'cannot_open', "$memberdir/members.ttl", 1 ); print {MEMTTL} qq~$membertotal|$latestmember~ or croak "$croak{'print'} MEMTTL"; fclose(MEMTTL); if (wantarray) { ManageMemberinfo('load'); ( $latestrealname, undef ) = split /\|/xsm, $memberinf{$latestmember}, 2; undef %memberinf; return ( $membertotal, $latestmember, $latestrealname ); } else { return $membertotal; } } sub RegApprovalCheck { ## alert admins and gmods of waiting users for approval if ( $regtype == 1 && ( $iamadmin || ( $iamgmod && $allow_gmod_admin eq 'on' && $gmod_access{'view_reglog'} eq 'on' ) ) ) { opendir MEM, "$memberdir"; my @approval = ( grep { /.wait$/ixsm } readdir MEM ); closedir MEM; my $app_waiting = $#approval + 1; if ( $app_waiting == 1 ) { $yyadmin_alert .= qq~
$reg_txt{'admin_alert_start_one'} $app_waiting $reg_txt{'admin_alert_one'} $reg_txt{'admin_alert_end'}
~; } elsif ( $app_waiting > 1 ) { $yyadmin_alert .= qq~
$reg_txt{'admin_alert_start_more'} $app_waiting $reg_txt{'admin_alert_more'} $reg_txt{'admin_alert_end_more'}
~; } } ## alert admins and gmods of waiting users for validations if ( ( $regtype == 1 || $regtype == 2 ) && ( $iamadmin || ( $iamgmod && $allow_gmod_admin eq 'on' && $gmod_access{'view_reglog'} eq 'on' ) ) ) { opendir MEM, "$memberdir"; my @preregged = ( grep { /.pre$/ixsm } readdir MEM ); closedir MEM; my $preregged_waiting = $#preregged + 1; if ( $preregged_waiting == 1 ) { $yyadmin_alert .= qq~
$reg_txt{'admin_alert_start_one'} $preregged_waiting $reg_txt{'admin_alert_act_one'} $reg_txt{'admin_alert_act_end'}
~; } elsif ( $preregged_waiting > 1 ) { $yyadmin_alert .= qq~
$reg_txt{'admin_alert_start_more'} $preregged_waiting $reg_txt{'admin_alert_act_more'} $reg_txt{'admin_alert_act_end_more'}
~; } } return; } sub activation_check { my ( $changed, $regtime, $regmember ); my $timespan = $preregspan * 3600; fopen( INACT, "$memberdir/memberlist.inactive" ); my @actlist = ; fclose(INACT); # check if user is in pre-registration and check activation key foreach (@actlist) { ( $regtime, undef, $regmember, undef ) = split /\|/xsm, $_, 4; if ( $date - $regtime > $timespan ) { $changed = 1; unlink "$memberdir/$regmember.pre"; # add entry to registration log fopen( REGLOG, ">>$vardir/registration.log", 1 ); print {REGLOG} "$date|T|$regmember|\n" or croak "$croak{'print'} REGLOG"; fclose(REGLOG); } else { # update non activate user list # write valid registration to the list again push @outlist, $_; } } if ($changed) { # re-open inactive list for update if changed fopen( INACT, ">$memberdir/memberlist.inactive", 1 ); print {INACT} @outlist or croak "$croak{'print'} INACT"; fclose(INACT); } return; } sub MakeStealthURL { # Usage is simple - just call MakeStealthURL with any url, and it will stealthify it. # if stealth urls are turned off, it just gives you the same value back my ($theurl) = @_; if ($stealthurl) { $theurl =~ s/([^\w\"\=\[\]]|[\n\b]|\A)\\*(\w+:\/\/[\w\~\.\;\:\,\$\-\+\!\*\?\/\=\&\@\#\%]+\.[\w\~\;\:\$\-\+\!\*\?\/\=\&\@\#\%]+[\w\~\;\:\$\-\+\!\*\?\/\=\&\@\#\%])/$boardurl\/$yyexec.$yyext?action=dereferer;url=$2/isgm; $theurl =~ s/([^\"\=\[\]\/\:\.(\:\/\/\w+)]|[\n\b]|\A)\\*(www\.[^\.][\w\~\.\;\:\,\$\-\+\!\*\?\/\=\&\@\#\%]+\.[\w\~\;\:\$\-\+\!\*\?\/\=\&\@\#\%]+[\w\~\;\:\$\-\+\!\*\?\/\=\&\@\#\%])/$boardurl\/$yyexec.$yyext?action=dereferer;url=http:\/\/$2/isgm; } return $theurl; } sub arraysort { # usage: &arraysort(1,"|","R",@array_to_sort); my ( $sortfield, $delimiter, $reverse, @in ) = @_; my ( @out, @sortkey, %newline, $n ); foreach my $oldline (@in) { my @sk = split /$delimiter/xsm, $oldline; $sk[$sortfield] = "$sk[$sortfield]-$n"; ## make sure that identical keys are avoided ## $n++; $newline{ $sk[$sortfield] } = $oldline; } @sortkey = sort keys %newline; if ($reverse) { @sortkey = reverse @sortkey; } foreach (@sortkey) { push @out, $newline{$_}; } return @out; } sub keygen { ## length = output length, type = A (All), U (Uppercase), L (lowercase) ## my ( $length, $type ) = @_; if ( $length <= 0 || $length > 10_000 || !$length ) { return; } $type = uc $type; if ( $type ne 'A' && $type ne 'U' && $type ne 'L' ) { $type = 'A'; } # generate random ID for password reset or other purposes. @chararray = qw(0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z); my $randid; for my $i ( 0 .. ( $length - 1 ) ) { $randid .= $chararray[ int rand 61 ]; } if ( $type eq 'U' ) { return uc $randid; } elsif ( $type eq 'L' ) { return lc $randid; } else { return $randid; } } ## Sticky Shimmy Shuffle by astro-pilot ## ## added to core on February 22, 2013 ## sub Rearrange_Sticky { my ( $i, $upstky, $downstky, $stkynum, $stky, @stickies, $oldboard ); $board = $INFO{'board'}; $stkynum = $INFO{'num'}; $direction = $INFO{'direction'}; $oldboard = $INFO{'oldboard'}; fopen( FILE, "$boardsdir/$board.txt" ) or fatal_error( "300 $messageindex_txt{'106'}: $messageindex_txt{'23'} $board.txt"); @threads = ; fclose(FILE); my $n = 0; foreach (@threads) { my ( $mnum, $msub, $mname, $memail, $mdate, $mreplies, $musername, $micon, $mstate ) = split /\|/xsm, $_; if ( $mstate =~ /(s|a)/ism && $mnum eq $stkynum ) { $stky = $n; } if ( $mstate =~ /(s|a)/ism ) { push @stickies, $_; $n++; } if ( $mstate =~ /s/ism ) { $_ = q{}; } } if ( $direction eq 'down' && $stky != $#stickies ) { $i = $stky; $i++; $downstky = $stickies[$stky]; $upstky = $stickies[$i]; $stickies[$stky] = $upstky; $stickies[$i] = $downstky; } if ( $direction eq 'up' && $stky != 0 ) { $i = $stky; $i--; $downstky = $stickies[$i]; $upstky = $stickies[$stky]; $stickies[$i] = $upstky; $stickies[$stky] = $downstky; } if ($oldboard) { @threads = @stickies; $currentboard = $oldboard; } else { push @threads, @stickies; } if ( ( $direction ne 'up' || $stky != 0 ) && ( $direction ne 'down' || $stky != $#stickies ) ) { fopen( FILE, ">$boardsdir/$board.txt" ) or fatal_error( "300 $messageindex_txt{'106'}: $messageindex_txt{'23'} $board.txt"); foreach (@threads) { chomp $_; next if /^(\s)*$/xsm; print {FILE} "$_\n" or croak "$croak{'print'} FILE"; } fclose(FILE); } $yySetLocation = qq~$scripturl?board=$currentboard;~; redirectexit(); return; } 1;