diff --git a/.gitignore b/.gitignore index 05af134..8164840 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ charmboard.conf # SQLite *.db -*.db-* \ No newline at end of file +*.db-* + +# Perl::Critic +perlcritic.log \ No newline at end of file diff --git a/.percriticrc b/.percriticrc new file mode 100644 index 0000000..8862995 --- /dev/null +++ b/.percriticrc @@ -0,0 +1,5 @@ +include = CodeLayout::RequireUseUTF8 CompileTime Documentation::RequirePodAtEnd + +severity = 5 +verbose = 5 +criticism-fatal = 1 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 5ed3be2..518df7f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" ] } \ No newline at end of file diff --git a/README.md b/README.md index e79b4d4..70ba6fd 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/charmboard.example.conf b/charmboard.example.conf index 2a57439..1a206b3 100644 --- a/charmboard.example.conf +++ b/charmboard.example.conf @@ -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 diff --git a/lib/CharmBoard.pm b/lib/CharmBoard.pm index c32d7ac..0f271e2 100644 --- a/lib/CharmBoard.pm +++ b/lib/CharmBoard.pm @@ -1,34 +1,20 @@ 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'); - + # load configuration from config file my $config = $self->plugin('Config' => {file => 'charmboard.conf'}); @@ -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, @@ -95,7 +81,7 @@ sub startup ($self) { $r->post('/login')->to( controller => 'Controller::Login', action => 'login_do'); - + ## logout $r->get('/logout')->to( controller => 'Controller::Logout', @@ -103,3 +89,5 @@ sub startup ($self) { } 1; + +__END__ \ No newline at end of file diff --git a/lib/CharmBoard/Controller/Index.pm b/lib/CharmBoard/Controller/Index.pm index 8f1ba0a..f864d68 100644 --- a/lib/CharmBoard/Controller/Index.pm +++ b/lib/CharmBoard/Controller/Index.pm @@ -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; -1; \ No newline at end of file + # 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__ \ No newline at end of file diff --git a/lib/CharmBoard/Controller/Login.pm b/lib/CharmBoard/Controller/Login.pm index acadd5f..3305cc5 100644 --- a/lib/CharmBoard/Controller/Login.pm +++ b/lib/CharmBoard/Controller/Login.pm @@ -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 ($self) { +sub login { + my $self = shift; + $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'); @@ -73,4 +73,10 @@ sub login_do ($self) { logged so the administrator can fix it.'); $self->redirect_to('login')}} -1; \ No newline at end of file +1; + +__END__ +=pod +=head1 NAME +CharmBoard::Controller::Login +=cut \ No newline at end of file diff --git a/lib/CharmBoard/Controller/Logout.pm b/lib/CharmBoard/Controller/Logout.pm index aa6084a..9e20740 100644 --- a/lib/CharmBoard/Controller/Logout.pm +++ b/lib/CharmBoard/Controller/Logout.pm @@ -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; diff --git a/lib/CharmBoard/Controller/Register.pm b/lib/CharmBoard/Controller/Register.pm index 3fdab24..2329412 100644 --- a/lib/CharmBoard/Controller/Register.pm +++ b/lib/CharmBoard/Controller/Register.pm @@ -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'); diff --git a/lib/CharmBoard/Crypt/Password.pm b/lib/CharmBoard/Crypt/Password.pm index daa3b9a..f70119c 100644 --- a/lib/CharmBoard/Crypt/Password.pm +++ b/lib/CharmBoard/Crypt/Password.pm @@ -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 @@ -53,16 +65,4 @@ the input password matched. Intended for login authentication or 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; \ No newline at end of file +=cut \ No newline at end of file diff --git a/lib/CharmBoard/Crypt/Seasoning.pm b/lib/CharmBoard/Crypt/Seasoning.pm index 347dff2..fcf79df 100644 --- a/lib/CharmBoard/Crypt/Seasoning.pm +++ b/lib/CharmBoard/Crypt/Seasoning.pm @@ -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]) { diff --git a/lib/CharmBoard/Schema.pm b/lib/CharmBoard/Schema.pm index f39ba17..29868bf 100644 --- a/lib/CharmBoard/Schema.pm +++ b/lib/CharmBoard/Schema.pm @@ -1,8 +1,14 @@ package CharmBoard::Schema; +use strict; +use warnings; +use experimental qw(try smartmatch); +use utf8; use base qw(DBIx::Class::Schema); __PACKAGE__->load_namespaces( result_namespace => 'Source', resultset_namespace => 'Set'); -1; \ No newline at end of file +1; + +__END__ \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Set/Categories.pm b/lib/CharmBoard/Schema/Set/Categories.pm new file mode 100644 index 0000000..e9f8911 --- /dev/null +++ b/lib/CharmBoard/Schema/Set/Categories.pm @@ -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__ \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Set/Subforums.pm b/lib/CharmBoard/Schema/Set/Subforums.pm new file mode 100644 index 0000000..f6a895e --- /dev/null +++ b/lib/CharmBoard/Schema/Set/Subforums.pm @@ -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__ \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Source/Categories.pm b/lib/CharmBoard/Schema/Source/Categories.pm index fc4213d..d57ffe5 100644 --- a/lib/CharmBoard/Schema/Source/Categories.pm +++ b/lib/CharmBoard/Schema/Source/Categories.pm @@ -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 \ No newline at end of file +1; +__END__ \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Source/Posts.pm b/lib/CharmBoard/Schema/Source/Posts.pm index 55a0398..8e82b16 100644 --- a/lib/CharmBoard/Schema/Source/Posts.pm +++ b/lib/CharmBoard/Schema/Source/Posts.pm @@ -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 \ No newline at end of file +1; +__END__ \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Source/Session.pm b/lib/CharmBoard/Schema/Source/Session.pm index 04378d6..caa32de 100644 --- a/lib/CharmBoard/Schema/Source/Session.pm +++ b/lib/CharmBoard/Schema/Source/Session.pm @@ -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 \ No newline at end of file +1; +__END__ \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Source/Subforums.pm b/lib/CharmBoard/Schema/Source/Subforums.pm index 8fd1949..bbefc63 100644 --- a/lib/CharmBoard/Schema/Source/Subforums.pm +++ b/lib/CharmBoard/Schema/Source/Subforums.pm @@ -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 \ No newline at end of file +1; +__END__ \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Source/Threads.pm b/lib/CharmBoard/Schema/Source/Threads.pm index 452bd0b..85d521a 100644 --- a/lib/CharmBoard/Schema/Source/Threads.pm +++ b/lib/CharmBoard/Schema/Source/Threads.pm @@ -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 \ No newline at end of file +1; +__END__ \ No newline at end of file diff --git a/lib/CharmBoard/Schema/Source/Users.pm b/lib/CharmBoard/Schema/Source/Users.pm index 5f0ef2e..81ab6b3 100644 --- a/lib/CharmBoard/Schema/Source/Users.pm +++ b/lib/CharmBoard/Schema/Source/Users.pm @@ -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 \ No newline at end of file +1; +__END__ \ No newline at end of file diff --git a/script/CharmBoard b/script/CharmBoard index 1d01019..6f7dc61 100755 --- a/script/CharmBoard +++ b/script/CharmBoard @@ -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 + diff --git a/templates/index.html.ep b/templates/index.html.ep index cdc44af..bf3acb8 100644 --- a/templates/index.html.ep +++ b/templates/index.html.ep @@ -1,2 +1,32 @@ % layout 'default', title => $self->boardName; -this is the index page \ No newline at end of file + +<% my $catHeader = begin %> + % my $_catID = shift; my $_name = shift; +