#!/usr/bin/perl --
# $Id: YaBBForum AdminIndex$
# $HeadURL: YaBB $
# $Source: /AdminIndex.pl $
###############################################################################
# AdminIndex.pl #
# $Date: 01 Sep 2026 $ #
###############################################################################
# YaBBForum: 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 YaBBForum (yabbforum.nz) - All Rights Reserved. #
# Software by: The YaBBForum Development Team #
# with assistance from the YaBBForum community. #
###############################################################################
no warnings qw(uninitialized once redefine);
use CGI::Carp qw(fatalsToBrowser);
use English qw(-no_match_vars);
# --- Master Version & Config ---
our $VERSION = '3.2';
my $github_user = "nixnutnz";
my $github_repo = "YaBBForum"; # Just the repository name
my $cache_file = "$vardir/github_update_cache.txt";
my $cache_ttl = 86400; # 24 hours in seconds
# --- Dynamic Variables (derived from $VERSION) ---
my $current_version = "v$VERSION";
$YaBBversion = "YaBBForum $VERSION";
$adminindexplver = "YaBBForum $VERSION";
use version;
if ( $action eq 'detailedversion' ) { return 1; }
$yyexec = 'index';
$script_root = $ENV{'SCRIPT_FILENAME'};
if ( !$script_root ) {
$script_root = $ENV{'PATH_TRANSLATED'};
}
$script_root =~ s/\/AdminIndex\.(pl|cgi)//igxsm;
push @INC, $script_root;
require Paths;
push @INC, "$boarddir/Modules";
if ( $ENV{'SERVER_SOFTWARE'} =~ /IIS/sm ) {
$yyIIS = 1;
if ( $PROGRAM_NAME =~ m{(.*)(\\|/)}xsm ) {
$yypath = $1;
}
$yypath =~ s/\\/\//gxsm;
chdir $yypath;
push @INC, $yypath;
}
$adminscreen = 1;
require Variables::Settings;
# Check always for Time::HiRes
eval { require Time::HiRes; import Time::HiRes qw(time); };
$START_TIME = time;
require "$boarddir/Sources/Subs.pm";
require Sources::System;
require Sources::DateTime;
require Sources::Load;
LoadCookie(); # Load the user's cookie (or set to guest)
LoadUserSettings(); # Load user settings
WriteLog(); # write into the logfile
WhatTemplate(); # Figure out which template to be using.
WhatLanguage(); # Figure out which language file we should be using!
get_micon();
if ($debug) { require Sources::Debug; }
if ($referersecurity) {
referer_check();
} # Check if the action is allowed from an external domain
require Sources::Security;
banning(); # Check for banned people
if ( !$maintenance && -e "$vardir/maintenance.lock" ) { $maintenance = 2; }
# some maintenance stuff will stop after $max_process_time
# in seconds, than the browser will call the script again
# until all is done. Don't put it too high or you will run
# into server or browser timeout. was 20
$max_process_time = 15;
$action = $INFO{'action'};
local $SIG{__WARN__} = sub { fatal_error( 'error_occurred', "@_" ); };
eval { yymain(); };
if ($@) { fatal_error( 'untrapped', ": $@" ); }
sub yymain {
# Choose what to do based on the form action
if ( $maintenance && $action eq 'login2' ) {
require Sources::LogInOut;
Login2();
}
# Do Sessions Checking
if ( !$iamguest && $sessions == 1 && $sessionvalid != 1 ) {
$yySetLocation = qq~$scripturl?action=revalidatesession~;
redirectexit();
}
# Other users can do nothing here.
if ( !$iamadmin && !$iamgmod ) {
if ($maintenance) { require Sources::LogInOut; InMaintenance(); }
$yySetLocation = qq~$scripturl~;
redirectexit();
}
if ($iamgmod) {
require "$vardir/gmodsettings.txt";
if ( !$allow_gmod_admin ) {
$yySetLocation = qq~$scripturl~;
redirectexit();
}
}
if ( $action ne q{} ) {
if ( $action eq $randaction ) {
require Sources::Decoder;
convert();
}
else {
require Admin::AdminSubList;
if ( $director{$action} ) {
my @act = split /&/xsm, $director{$action};
require "$admindir/$act[0]";
&{ $act[1] };
}
else {
require Admin::Admin;
Admin();
}
}
}
else {
TrackAdminLogins();
require Admin::Admin;
Admin();
}
return;
}
sub ParseNavArray {
my @x = @_;
foreach my $element (@x) {
chomp $element;
( $action_to_take, $vistext, $whatitdoes, $isheader ) =
split /\|/xsm, $element;
if ( $action_area eq $action_to_take ) {
$currentclass = 'class="current"';
}
else {
$currentclass = q{};
}
if ($isheader) {
$started_ul = 1;
$leftmenu .= qq~
~;
}
return;
}
sub AdmImgLoc {
my ($img) = @_;
if ( !-e "$htmldir/Templates/Forum/$useimages/$img" ) {
$thisimgloc = qq~img src="$yyhtml_root/Templates/Forum/default/$img"~;
}
else { $thisimgloc = qq~img src="$imagesdir/$img"~; }
return $thisimgloc;
}
sub AdmImgLoc2 {
my ($img) = @_;
if ( !-e "$htmldir/Templates/Forum/$useimages/$img" ) {
$thisimgloc = qq~$yyhtml_root/Templates/Forum/default/$img~;
}
else { $thisimgloc = qq~$imagesdir/$img~; }
return $thisimgloc;
}
sub AdminTemplate {
$admin_template = ${ $uid . $username }{'template'};
if ( !-d "$htmldir/Templates/Admin/$admin_template"
|| $admin_template eq q{} )
{
$admin_template = 'default';
}
$adminstyle =
qq~ ~;
$adminstyle =~ s/$admin_template\///gxsm;
$adminimages = qq~$yyhtml_root/Templates/Admin/$admin_template~;
$adminimages =~ s/$admin_template\///gxsm;
require "$templatesdir/$admin_template/AdminCentre.template";
require "$vardir/gmodsettings.txt";
@forum_settings = (
"|$admintxt{'a1_title'}|$admintxt{'a1_label'} - $admintxt{'34'}|a1",
"newsettings;page=main|$admintxt{'a1_sub1'}|$admintxt{'a1_label1'}|",
"newsettings;page=advanced|$admintxt{'a1_sub2'}|$admintxt{'a1_label2'}|",
"editpaths|$admintxt{'a1_sub3'}|$admintxt{'a1_label3'}|",
"editbots|$admintxt{'a1_sub4'}|$admintxt{'a1_label4'}|",
);
if ($extendedprofiles) {
splice @forum_settings, 3, 0,
"ext_admin|$admintxt{'a1_sub_ex'}|$admintxt{'a1_label_ex'}|";
}
@general_controls = (
"|$admintxt{'a2_title'}|$admintxt{'a2_label'} - $admintxt{'34'}|a2",
"newsettings;page=news|$admintxt{'a2_sub1'}|$admintxt{'a2_label1'}|",
"smilies|$admintxt{'a2_sub2'}|$admintxt{'a2_label2'}|",
"setcensor|$admintxt{'a2_sub3'}|$admintxt{'a2_label3'}|",
"modagreement|$admintxt{'a2_sub4'}|$admintxt{'a2_label4'}|",
"gmodaccess|$admintxt{'a2_sub5'}|$admintxt{'a2_label5'}|",
"eventcal_set|$admintxt{'a2_sub6'}|$admintxt{'a2_label6'}|",
"bookmarks|$admintxt{'bookmarks'}|$admintxt{'bookmarks1'}|"
);
@security_settings = (
"|$admintxt{'a3_title'}|$admintxt{'a3_label'} - $admintxt{'34'}|a3",
"newsettings;page=security|$admintxt{'a3_sub2'}|$admintxt{'a3_label2'}|",
"referer_control|$admintxt{'a3_sub1'}|$admintxt{'a3_label1'}|",
"setup_guardian|$admintxt{'a3_sub3'}|$admintxt{'a3_label3'}|",
"newsettings;page=antispam|$admintxt{'a3_sub4'}|$admintxt{'a3_label4'}|",
"spam_questions|$admintxt{'a3_sub6'}|$admintxt{'a3_label6'}|",
"setreserve|$admintxt{'a6_sub6'}|$admintxt{'a6_label6'}|",
);
@forum_controls = (
"|$admintxt{'a4_title'}|$admintxt{'a4_label'} - $admintxt{'34'}|a4",
"managecats|$admintxt{'a4_sub1'}|$admintxt{'a4_label1'}|",
"manageboards|$admintxt{'a4_sub2'}|$admintxt{'a4_label2'}|",
"helpadmin|$admintxt{'a4_sub3'}|$admintxt{'a4_label3'}|",
"editemailtemplates|$admintxt{'a4_sub4'}|$admintxt{'a4_label4'}|",
);
@forum_layout = (
"|$admintxt{'a5_title'}|$admintxt{'a5_label'} - $admintxt{'34'}|a5",
"modskin|$admintxt{'a5_sub1'}|$admintxt{'a5_label1'}|",
"modcss|$admintxt{'a5_sub2'}|$admintxt{'a5_label2'}|",
);
@member_controls = (
"|$admintxt{'a6_title'}|$admintxt{'a6_label'} - $admintxt{'34'}|a6",
"addmember|$admintxt{'a6_sub1'}|$admintxt{'a6_label1'}|",
"view_reglog|$admintxt{'a8_sub5'}|$admintxt{'a8_label5'}|",
"viewmembers|$admintxt{'a6_sub2'}|$admintxt{'a6_label2'}|",
"modmemgr|$admintxt{'a6_sub3'}|$admintxt{'a6_label3'}|",
"mailing|$admintxt{'a6_sub4'}|$admintxt{'a6_label4'}|",
"ipban|$admintxt{'a6_sub5'}|$admintxt{'a6_label5'}|",
);
@maintence_controls = (
"|$admintxt{'a7_title'}|$admintxt{'a7_label'} - $admintxt{'34'}|a7",
"newsettings;page=maintenance|$admin_txt{'67'}|$admin_txt{'67'}|",
"backupsettings|$admintxt{'a3_sub5'}|$admintxt{'a3_label5'}|",
"rebuildmesindex|$admintxt{'a7_sub2a'}|$admintxt{'a7_label2a'}|",
"boardrecount|$admintxt{'a7_sub2'}|$admintxt{'a7_label2'}|",
"rebuildmemlist|$admintxt{'a7_sub4'}|$admintxt{'a7_label4'}|",
"membershiprecount|$admintxt{'a7_sub3'}|$admintxt{'a7_label3'}|",
"rebuildmemhist|$admintxt{'a7_sub4a'}|$admintxt{'a7_label4a'}|",
"rebuildnotifications|$admintxt{'a7_sub4b'}|$admintxt{'a7_label4b'}|",
"clean_log|$admintxt{'a7_sub1'}|$admintxt{'a7_label1'}|",
"deleteoldthreads|$admintxt{'a7_sub5'}|$admintxt{'a7_label5'}|",
"manageattachments|$admintxt{'a7_sub6'}|$admintxt{'a7_label6'}|",
);
@forum_stats = (
"|$admintxt{'a8_title'}|$admintxt{'a8_label'} - $admintxt{'34'}|a8",
"detailedversion|$admintxt{'a8_sub1'}|$admintxt{'a8_label1'}|",
"stats|$admintxt{'a8_sub2'}|$admintxt{'a8_label2'}|",
"showclicks|$admintxt{'a8_sub3'}|$admintxt{'a8_label3'}|",
"errorlog|$admintxt{'a8_sub4'}|$admintxt{'a8_label4'}|",
);
@boardmod_mods = (
"|$admintxt{'a9_title'}|$admintxt{'a9_label'} - $admintxt{'34'}|a9",
"modlist|$mod_list{'6'}|$mod_list{'7'}|",
);
# To add new items for your mods settings, add a new row below here, pushing
# your item onto the @boardmod_mods array. Example below:
# $my_mod = "action_to_take|Name_Displayed|Tooltip_Title|";
# push (@boardmod_mods, "$my_mod");
# before the first pipe character is the action that will appear in the URL
# Next is the text that is displayed in the admin centre
# Finally, you have the tooltip text, necessary for XHTML compliance
# Also note, you should pick a unique name instead of "$my_mod".
# If you mod is called "SuperMod For Doing Cool Things"
# You could use "$SuperMod_CoolThings"
### BOARDMOD ANCHOR ###
### END BOARDMOD ANCHOR ###
ParseNavArray(@member_controls);
ParseNavArray(@maintence_controls);
ParseNavArray(@forum_settings);
ParseNavArray(@general_controls);
ParseNavArray(@security_settings);
ParseNavArray(@forum_controls);
ParseNavArray(@forum_layout);
ParseNavArray(@forum_stats);
ParseNavArray(@boardmod_mods);
$topmenu_one = qq~$admintxt{'15'} $mbname ~;
$topmenu_two = qq~$admintxt{'33'} ~;
$topmenu_tree =
qq~$admintxt{'35'} ~;
$topmenu_four = qq~$admintxt{'36'} ~;
if ($maintenance && $action ne 'detailedversion') {
$yyadmin_alert .=
qq~$load_txt{'616a'} ~;
}
if ( $iamadmin && $rememberbackup && $action ne 'detailedversion' ) {
if ( $lastbackup && $date > $rememberbackup + $lastbackup ) {
require Sources::DateTime;
$yyadmin_alert .=
qq~$load_txt{'617'} ~
. timeformat($lastbackup)
. q~ ~;
}
}
print_output_header();
my $yytitle = qq~$mbname $admin_txt{'208'}: $yytitle~;
$header =~ s/(\{|<)yabb\ title(}|>)/$yytitle/gxsm;
$header =~ s/(\{|<)yabb\ style(}|>)/$adminstyle/gxsm;
$header =~ s/(\{|<)yabb\ charset(}|>)/$yymycharset/gxsm;
$header =~ s/(\{|<)yabb\ javascript(}|>)/$yyjavascript/gxsm;
$leftmenutop =~ s/(\{|<)yabb\ images(}|>)/$adminimages/gxsm;
$leftmenutop =~ s/(\{|<)yabb\ maintenance(}|>)/$yyadmin_alert/gxsm;
$topnav =~ s/(\{|<)yabb\ topmenu_one(}|>)/$topmenu_one/xsm;
$topnav =~ s/(\{|<)yabb\ topmenu_two(}|>)/$topmenu_two/xsm;
$topnav =~ s/(\{|<)yabb\ topmenu_tree(}|>)/$topmenu_tree/xsm;
$topnav =~ s/(\{|<)yabb\ topmenu_four(}|>)/$topmenu_four/xsm;
$topnav =~ s/(\{|<)yabb\ brdname(}|>)/$mbname/xsm;
if ($debug) { Debug(); }
$mainbody =~ s/(\{|<)yabb\ main(}|>)/$yymain/gxsm;
$mainbody =~ s/(\{|<)yabb_admin\ debug(}|>)/$yydebug/gxsm;
$mainbody =~ s/img src\=\"$imagesdir\/(.+?)\"/AdmImgLoc($1)/eisgm;
$mainbody =~
s/img src\=\"\;$imagesdir\/(.+?)\"/"img src\=\"" . AdmImgLoc2($1) . "\""/eisgm;
# For the template editing Javascript images
$output =
$header
. $leftmenutop
. $leftmenu
. $leftmenubottom
. $topnav
. $mainbody;
image_resize();
print_HTML_output_and_finish();
return;
}
sub TrackAdminLogins {
if ( -e "$vardir/adminlog_new.txt" ) {
fopen( ADMINLOG, "$vardir/adminlog_new.txt" );
@adminlog = ;
fclose(ADMINLOG);
@adminlog = reverse sort @adminlog;
}
$maxadminlog = $maxadminlog || 5;
fopen( ADMINLOG, ">$vardir/adminlog_new.txt" );
print {ADMINLOG} qq~$date|$username|$user_ip\n~
or croak 'cannot print ADMINLOG';
for my $i ( 0 .. ( $maxadminlog - 2 ) ) {
if ( $adminlog[$i] ) {
chomp $adminlog[$i];
print {ADMINLOG} qq~$adminlog[$i]\n~
or croak 'cannot print ADMINLOG';
}
}
fclose(ADMINLOG);
return;
}
use LWP::UserAgent;
use JSON::PP;
sub check_github_updates {
# Ensure we use YaBB's global $vardir, or fall back to 'Variables'
my $target_dir = $vardir || 'Variables';
my $cache_file = "$target_dir/github_update_cache.txt";
my $cache_ttl = 86400; # 24 hours in seconds
# 1. Read from cache if it exists and is less than 24h old
if (-e $cache_file && (time() - (stat($cache_file))[9]) < $cache_ttl) {
if (open(my $fh, '<', $cache_file)) {
my $cached_html = do { local $/; <$fh> };
close($fh);
return $cached_html if $cached_html;
}
}
my $output_html = '';
my $ua = LWP::UserAgent->new(timeout => 5);
$ua->agent("YaBBForum-UpdateChecker/$VERSION");
# 2. Query GitHub Releases API for Official Tagged Releases
my $release_url = "https://api.github.com/repos/$github_user/$github_repo/releases/latest";
my $response = $ua->get($release_url);
if ($response->is_success) {
my $data = eval { decode_json($response->decoded_content) };
if ($data && $data->{tag_name}) {
my $latest_tag = $data->{tag_name};
my $release_page = $data->{html_url};
my $zipball_url = $data->{zipball_url} || "https://github.com/$github_user/$github_repo/archive/refs/tags/$latest_tag.zip";
my $changelog = $data->{body} || "No release notes provided.";
$changelog =~ s/\r?\n/ /g;
# --- NEW CODE ---
(my $v_latest_str = $latest_tag) =~ s/^v//i;
(my $v_current_str = $current_version) =~ s/^v//i;
# Parse into version objects safely
my $v_latest = eval { version->parse($v_latest_str) };
my $v_current = eval { version->parse($v_current_str) };
# Only show update notice if the GitHub release is strictly GREATER than current version
if ($v_latest && $v_current && $v_latest > $v_current) {
$output_html .= qq{
};
}
}
}
# 3. Query GitHub Commits API for Recent Code Changes / Updated Files
my $commits_url = "https://api.github.com/repos/$github_user/$github_repo/commits?per_page=1";
my $commit_resp = $ua->get($commits_url);
if ($commit_resp->is_success) {
my $commits = eval { decode_json($commit_resp->decoded_content) };
if ($commits && ref($commits) eq 'ARRAY' && scalar(@$commits) > 0) {
my $last_commit = $commits->[0];
my $commit_msg = $last_commit->{commit}->{message} || 'No commit message';
my $commit_date = substr($last_commit->{commit}->{committer}->{date}, 0, 10);
my $commit_web = $last_commit->{html_url};
my $repo_zip_url = "https://github.com/$github_user/$github_repo/archive/refs/heads/main.zip";
# Keep only the first line of the commit message for display
$commit_msg =~ s/\n.*//s;
$output_html .= qq{
};
}
}
# 4. Fallback if board is up-to-date and no release banner was generated
if (!$output_html) {
$output_html = qq{
✅ YaBBForum $current_version is fully up to date!
};
}
# 5. Save combined HTML output to cache file
if (open(my $fh, '>', $cache_file)) {
print $fh $output_html;
close($fh);
}
return $output_html;
}
1;