Merge commit 'b7c57b277b2097675c40ae71c80abf464b540668' into HEAD

This commit is contained in:
ngoomie 2023-05-15 18:04:31 -06:00
commit e12eeebb6e
23 changed files with 306 additions and 121 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ charmboard.conf
# SQLite
*.db
*.db-*
# Perl::Critic
perlcritic.log

5
.percriticrc Normal file
View File

@ -0,0 +1,5 @@
include = CodeLayout::RequireUseUTF8 CompileTime Documentation::RequirePodAtEnd
severity = 5
verbose = 5
criticism-fatal = 1

21
.vscode/settings.json vendored
View File

@ -1,4 +1,5 @@
{
"editor.tabSize": 2,
"cSpell.enableFiletypes": [
"mojolicious",
"perl"
@ -8,26 +9,19 @@
"Authen",
"CharmBoard",
"Facepunch",
"listsubf",
"passchk",
"passgen",
"pgsql",
"resultset",
"signup",
"subf",
"subforum",
"subforums"
"subforums",
"subfs"
],
"better-comments.highlightPlainText": true,
"better-comments.tags": [
{
"tag": "!",
"color": "#FF2D00",
"strikethrough": false,
"underline": false,
"backgroundColor": "transparent",
"bold": false,
"italic": false
},
{
"tag": "?",
"color": "#3498DB",
@ -55,5 +49,10 @@
"bold": false,
"italic": false
}
],
"perl-toolbox.lint.perlcriticProfile": "$workspaceRoot/.perlcriticrc",
"perl-toolbox.lint.useProfile": true,
"perl-toolbox.syntax.includePaths": [
"$workspaceRoot/libs"
]
}

View File

@ -4,12 +4,13 @@ CharmBoard is forum software written in Perl, inspired by AcmlmBoard/its derivat
## Requirements
- Perl5 v5.20.0 or higher
- Perl5
- `Mojolicious` ([website](https://www.mojolicious.org/), [metacpan](https://metacpan.org/pod/Mojolicious))
- `Mojolicious::Plugin::Renderer::WithoutCache` — only needed in dev environment
- `DBI`
- `DBIx::Class`
- one of two `DBD` database drivers — see `INSTALLING.md` for detailed information
- `Tree::Simple`
- `Authen::Passphrase::Argon2`
- `Math::Random::Secure`

View File

@ -2,14 +2,14 @@
board_name => '',
database => {
type => '', # 'sqlite' or 'mysql'
type => '', # 'sqlite' or 'mariadb'
name => '',
user => '',
pass => ''
},
pass_crypt => {
pepper => '' # generate this with `tools/pepper.pl` for now
pepper => ''
},
environment => '', # only use 'dev' for now

View File

@ -1,31 +1,17 @@
package CharmBoard;
use utf8;
use experimental 'try', 'smartmatch';
use strict;
use warnings;
use experimental qw(try smartmatch);
use Mojo::Base 'Mojolicious', -signatures;
use CharmBoard::Schema;
=pod
=head1 NAME
CharmBoard - revive the fun posting experience!
=head1 NOTES
This documentation is intended for prospective code
contributors. If you're looking to set CharmBoard up,
look for the Markdown format (.md) documentation instead.
CharmBoard uses a max line length of 60 chars and a tab
size of two spaces.
=head1 DESCRIPTION
CharmBoard is forum software written in Perl with
Mojolicious, intended to be a more fun alternative to the
bigger forum suites available today, inspired by older
forum software like AcmlmBoard, while also being more
modernized in terms of security practices than they are.
Customization ability is another important goal next to
making software that feels fun for the end user to use.
=cut
# this method will run once at server start
sub startup ($self) {
sub startup {
my $self = shift;
# load plugins that require no additional conf
$self->plugin('TagHelpers');
@ -54,7 +40,7 @@ sub startup ($self) {
$dsn = "dbi:SQLite:" . $config->{database}->{name};
$dbUnicode = "sqlite_unicode"}
elsif ($self->config->{database}->{type} ~~ 'mysql') {
elsif ($self->config->{database}->{type} ~~ 'mariadb') {
$dsn = "dbi:mysql:" . $config->{database}->{name};
$dbUnicode = "mysql_enable_utf"}
@ -62,7 +48,7 @@ sub startup ($self) {
in charmboard.conf. If you're sure you've set it to
something supported, maybe double check your spelling?
\n\n\t
Valid options: 'sqlite', 'mysql'"};
Valid options: 'sqlite', 'mariadb'"};
my $schema = CharmBoard::Schema->connect(
$dsn,
@ -103,3 +89,5 @@ sub startup ($self) {
}
1;
__END__

View File

@ -1,11 +1,45 @@
package CharmBoard::Controller::Index;
use utf8;
use experimental 'try', 'smartmatch';
use strict;
use warnings;
use feature qw(say unicode_strings);
use experimental qw(try smartmatch);
use Mojo::Base 'Mojolicious::Controller', -signatures;
use Tree::Simple;
sub index ($self) {
$self->render(template => 'index')
sub index {
my $self = shift;
}
# fetch a list of all categories
my @allCat =
$self->schema->resultset('Categories')->fetch_all;
# create a Tree::Simple object that will contain the list
# of categories and the subforums that belong to them
my $tree =
Tree::Simple->new("subfList", Tree::Simple->ROOT);
my ($fetchSubf, $catBranch);
foreach my $iterCat (@allCat) {
# create branch of subfList for the current category
$catBranch =
Tree::Simple->new($iterCat, $tree);
# fetch all subforums that belong to this category
$fetchSubf =
$self->schema->resultset('Subforums')
->fetch_by_cat($iterCat);
# add each fetched subforum as children of the branch
# for the current category
foreach my $iterSubf ($fetchSubf) {
Tree::Simple->new($iterSubf, $catBranch)}}
$self->render(
template => 'index',
categoryTree => $tree)}
1;
__END__

View File

@ -1,22 +1,22 @@
package CharmBoard::Controller::Login;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures;
use CharmBoard::Crypt::Password;
use CharmBoard::Crypt::Seasoning;
=pod
=head1 NAME
CharmBoard::Controller::Login
=cut
sub login {
my $self = shift;
sub login ($self) {
$self->render(
template => 'login',
error => $self->flash('error'),
message => $self->flash('message'))};
sub login_do ($self) {
sub login_do {
my $self = shift;
my $username = $self->param('username');
my $password = $self->pepper . ':' . $self->param('password');
@ -74,3 +74,9 @@ sub login_do ($self) {
$self->redirect_to('login')}}
1;
__END__
=pod
=head1 NAME
CharmBoard::Controller::Login
=cut

View File

@ -1,9 +1,13 @@
package CharmBoard::Controller::Logout;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub logout_do ($self) {
sub logout_do {
my $self = shift;
# destroy entry for this session in the database
$self->schema->resultset('Session')->search({
session_key => $self->session('session_key')})->delete;

View File

@ -1,18 +1,23 @@
package CharmBoard::Controller::Register;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures;
use CharmBoard::Crypt::Password;
# initial registration page
sub register ($self) {
sub register {
my $self = shift;
$self->render(
template => 'register',
error => $self->flash('error'),
message => $self->flash('message'))};
# process submitted registration form
sub register_do ($self) {
sub register_do {
my $self = shift;
my $username = $self->param('username');
my $email = $self->param('email');
my $password = $self->param('password');

View File

@ -1,4 +1,7 @@
package CharmBoard::Crypt::Password;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use Authen::Passphrase::Argon2;
use CharmBoard::Crypt::Seasoning;
@ -6,6 +9,31 @@ use CharmBoard::Crypt::Seasoning;
use Exporter qw(import);
our @EXPORT = qw(passgen passchk);
sub passgen {
my $argon2 = Authen::Passphrase::Argon2->new(
salt => seasoning(32),
passphrase => $_[0],
cost => 17,
factor => '32M',
parallelism => 1,
size => 32 );
return ($argon2->salt_hex, $argon2->hash_hex)};
sub passchk {
my $argon2 = Authen::Passphrase::Argon2->new(
salt_hex => $_[0],
hash_hex => $_[1],
cost => 17,
factor => '32M',
parallelism => 1,
size => 32 );
return ($argon2->match($_[2]))}
1;
__END__
=pod
=head1 NAME
CharmBoard::Crypt::Password - password processing module
@ -23,27 +51,11 @@ when logging in to make sure they're correct.
Currently the only available password hashing scheme is Argon2, but
this might be changed later on.
=cut
=pod
=head2 passgen
passgen is the function for generating password salts and hashes to
be inserted into the database. It takes the plaintext password you
wish to hash as the only argument, and outputs the salt and
Argon2 hash string in hexadecimal form.
=cut
sub passgen ($) {
my $argon2 = Authen::Passphrase::Argon2->new(
salt => seasoning(32),
passphrase => $_[0],
cost => 17,
factor => '32M',
parallelism => 1,
size => 32 );
return ($argon2->salt_hex, $argon2->hash_hex)};
=pod
=head2 passchk
passchk is the function for checking plaintext passwords against the
hashed password + salt already stored in the database. It takes the
@ -54,15 +66,3 @@ anywhere else where one may need to verify passwords (i.e. before
changing existing passwords, or for admins confirming they wish to
perform a risky or nonreversible operation.)
=cut
sub passchk ($$$) {
my $argon2 = Authen::Passphrase::Argon2->new(
salt_hex => $_[0],
hash_hex => $_[1],
cost => 17,
factor => '32M',
parallelism => 1,
size => 32 );
return ($argon2->match($_[2]))}
1;

View File

@ -1,15 +1,18 @@
package CharmBoard::Crypt::Seasoning;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use Math::Random::Secure qw(irand);
use Exporter qw(import);
our @EXPORT = qw(seasoning);
sub seasoning ($) {
sub seasoning {
my @spices = 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 $blend;
while (length($blend) < $_[0]) {

View File

@ -1,4 +1,8 @@
package CharmBoard::Schema;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Schema);
__PACKAGE__->load_namespaces(
@ -6,3 +10,5 @@ __PACKAGE__->load_namespaces(
resultset_namespace => 'Set');
1;
__END__

View File

@ -0,0 +1,29 @@
package CharmBoard::Schema::Set::Categories;
use utf8;
use strict;
use warnings;
use feature qw(say unicode_strings);
use experimental qw(try smartmatch);
use base 'DBIx::Class::ResultSet';
sub fetch_all {
my $set = shift;
my $_fetch =
$set->search({},
{order_by => 'cat_rank'});
return($_fetch->get_column('cat_id')->all)}
sub title_from_id {
my $set = shift;
return(
$set->search({'cat_id' => $_[0]})->
get_column('cat_name')->first)}
1;
__END__

View File

@ -0,0 +1,30 @@
package CharmBoard::Schema::Set::Subforums;
use utf8;
use strict;
use warnings;
use feature qw(say unicode_strings);
use experimental qw(try smartmatch);
use base 'DBIx::Class::ResultSet';
sub fetch_by_cat {
my $set = shift;
my $fetch =
$set->search(
{'subf_cat' => $_[0] },
{order_by => 'subf_rank',
group_by => undef});
return($fetch->get_column('subf_id')->all)}
sub title_from_id {
my $set = shift;
return(
$set->search({'subf_id' => $_[0]})->
get_column('subf_name')->first)}
1;
__END__

View File

@ -1,4 +1,8 @@
package CharmBoard::Schema::Source::Categories;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core);
__PACKAGE__->table('categories');
@ -7,10 +11,14 @@ __PACKAGE__->add_columns(
data_type => 'integer',
is_auto_increment => 1,
is_nullable => 0, },
cat_rank => {
data_type => 'integer',
is_nullable => 0, },
cat_name => {
data_type => 'text',
is_nullable => 0, });
__PACKAGE__->set_primary_key('cat_id');
1
1;
__END__

View File

@ -1,27 +1,26 @@
package CharmBoard::Schema::Source::Posts;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core);
__PACKAGE__->table('posts');
__PACKAGE__->add_columns(
post_id => {
data_type => 'integer',
is_foreign_key => 0,
is_auto_increment => 1,
is_nullable => 0, },
user_id => {
data_type => 'integer',
is_foreign_key => 1,
is_auto_increment => 0,
is_nullable => 0, },
thread_id => {
data_type => 'integer',
is_foreign_key => 1,
is_auto_increment => 0,
is_nullable => 0, },
post_date => {
data_type => 'integer',
is_foreign_key => 0,
is_auto_increment => 0,
is_nullable => 0, });
__PACKAGE__->set_primary_key('post_id');
@ -35,4 +34,5 @@ __PACKAGE__->belongs_to(
'CharmBoard::Schema::Source::Threads',
'thread_id' );
1
1;
__END__

View File

@ -1,27 +1,26 @@
package CharmBoard::Schema::Source::Session;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core);
__PACKAGE__->table('sessions');
__PACKAGE__->add_columns(
session_key => {
data_type => 'text',
is_auto_increment => 0,
is_nullable => 0, },
user_id => {
data_type => 'integer',
is_auto_increment => 0,
is_nullable => 0, },
session_expiry => {
data_type => 'numeric',
is_auto_increment => 0,
is_nullable => 0, },
is_ip_bound => {
data_type => 'integer',
is_auto_increment => 0,
is_nullable => 0, },
bound_ip => {
data_type => 'text',
is_auto_increment => 0,
is_nullable => 1, });
__PACKAGE__->set_primary_key('session_key');
@ -31,4 +30,5 @@ __PACKAGE__->belongs_to(
'CharmBoard::Schema::Source::Users',
'user_id');
1
1;
__END__

View File

@ -1,4 +1,8 @@
package CharmBoard::Schema::Source::Subforums;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core);
__PACKAGE__->table('subforums');
@ -10,15 +14,16 @@ __PACKAGE__->add_columns(
subf_cat => {
data_type => 'integer',
is_foreign_key => 1,
is_auto_increment => 0,
is_nullable => 0, },
subf_rank => {
data_type => 'integer',
is_numeric => 1,
is_nullable => 0, },
subf_name => {
data_type => 'text',
is_auto_increment => 0,
is_nullable => 0, },
subf_desc => {
data_type => 'text',
is_auto_increment => 0,
is_nullable => 1, });
__PACKAGE__->set_primary_key('subf_id');
@ -28,4 +33,5 @@ __PACKAGE__->belongs_to(
'CharmBoard::Schema::Source::Categories',
{'foreign.cat_id' => 'self.subf_cat'});
1
1;
__END__

View File

@ -1,4 +1,8 @@
package CharmBoard::Schema::Source::Threads;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core);
__PACKAGE__->table('threads');
@ -13,9 +17,7 @@ __PACKAGE__->add_columns(
thread_subf => {
data_type => 'integer',
is_foreign_key => 1,
is_nullable => 1, });
# ! thread_subf should NOT be nullable once subforums
# ! are properly implemented
is_nullable => 0, });
__PACKAGE__->set_primary_key('thread_id');
@ -24,4 +26,5 @@ __PACKAGE__->belongs_to(
'CharmBoard::Schema::Source::Subforums',
{'foreign.subf_id' => 'self.thread_subf'});
1
1;
__END__

View File

@ -1,4 +1,7 @@
package CharmBoard::Schema::Source::Users;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core);
@ -11,19 +14,15 @@ __PACKAGE__->add_columns(
is_auto_increment => 1, },
username => {
data_type => 'text',
is_numeric => 0,
is_nullable => 0, },
email => {
data_type => 'text',
is_numeric => 0,
is_nullable => 0, },
password => {
data_type => 'text',
is_numeric => 0,
is_nullable => 0, },
salt => {
data_type => 'text',
is_numeric => 0,
is_nullable => 0, },
signup_date => {
data_type => 'integer',
@ -32,4 +31,5 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key('user_id');
1
1;
__END__

View File

@ -1,5 +1,5 @@
#!/usr/bin/env perl
use experimental 'try', 'smartmatch';
use experimental qw(try smartmatch);
use strict;
use warnings;
use utf8;
@ -10,3 +10,28 @@ use Mojolicious::Commands;
# Start command line interface for application
Mojolicious::Commands->start_app('CharmBoard');
__END__
=pod
=head1 NAME
CharmBoard - revive the fun posting experience!
=head1 NOTES
This documentation is intended for prospective code
contributors. If you're looking to set CharmBoard up,
look for the Markdown format (.md) documentation instead.
CharmBoard uses a max line length of 60 chars and a tab
size of two spaces.
=head1 DESCRIPTION
CharmBoard is forum software written in Perl with
Mojolicious, intended to be a more fun alternative to the
bigger forum suites available today, inspired by older
forum software like AcmlmBoard, while also being more
modernized in terms of security practices than they are.
Customization ability is another important goal next to
making software that feels fun for the end user to use.
=cut

View File

@ -1,2 +1,32 @@
% layout 'default', title => $self->boardName;
this is the index page
<% my $catHeader = begin %>
% my $_catID = shift; my $_name = shift;
<div class="category-header category-<%= $_catID %>">
<b><%= $_name %></b>
</div>
<% end %>
<% my $subfItem = begin %>
% my $_subfID = shift; my $_catID = shift;
% my $_name = shift;
<div class="
subforum-item subforum-<%= $_subfID %>
category-<%= $_catID %>
"><%= $_name %></div>
<% end %>
<%
foreach my $category ($categoryTree->getAllChildren) { %>
<%= $catHeader->(
$category->getNodeValue,
$self->schema->resultset('Categories')->
title_from_id($category->getNodeValue)) %>
<%
foreach my $subforum ($category->getAllChildren) { %>
<%= $subfItem->(
$subforum->getNodeValue,
$category->getNodeValue,
$self->schema->resultset('Subforums')->
title_from_id($subforum->getNodeValue)) %>
<% }} %>