############################################################################### # System.pm # # $Date: 01 Sep 2026 $ # ############################################################################### # YaBB: Yet another Bulletin Board # # Open-Source Community Software for Webmasters # # Version: YaBBForum 3.2 # # 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.2'; $systempmver = 'YaBBForum 3.2'; 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 X 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;