Compare commits

...

2 Commits

23 changed files with 317 additions and 346 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ charmboard.conf
# SQLite # SQLite
*.db *.db
*.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": [ "cSpell.enableFiletypes": [
"mojolicious", "mojolicious",
"perl" "perl"
@ -8,28 +9,19 @@
"Authen", "Authen",
"CharmBoard", "CharmBoard",
"Facepunch", "Facepunch",
"listsubf",
"passchk", "passchk",
"passgen", "passgen",
"pgsql",
"resultset", "resultset",
"signup", "signup",
"SMALLINT",
"subf", "subf",
"subforum", "subforum",
"subforums", "subforums",
"TINYTEXT" "subfs"
], ],
"better-comments.highlightPlainText": true, "better-comments.highlightPlainText": true,
"better-comments.tags": [ "better-comments.tags": [
{
"tag": "!",
"color": "#FF2D00",
"strikethrough": false,
"underline": false,
"backgroundColor": "transparent",
"bold": false,
"italic": false
},
{ {
"tag": "?", "tag": "?",
"color": "#3498DB", "color": "#3498DB",
@ -57,5 +49,10 @@
"bold": false, "bold": false,
"italic": 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 ## Requirements
- Perl5 v5.20.0 or higher - Perl5
- `Mojolicious` ([website](https://www.mojolicious.org/), [metacpan](https://metacpan.org/pod/Mojolicious)) - `Mojolicious` ([website](https://www.mojolicious.org/), [metacpan](https://metacpan.org/pod/Mojolicious))
- `Mojolicious::Plugin::Renderer::WithoutCache` — only needed in dev environment - `Mojolicious::Plugin::Renderer::WithoutCache` — only needed in dev environment
- `DBI` - `DBI`
- `DBIx::Class` - `DBIx::Class`
- one of two `DBD` database drivers — see `INSTALLING.md` for detailed information - one of two `DBD` database drivers — see `INSTALLING.md` for detailed information
- `Tree::Simple`
- `Authen::Passphrase::Argon2` - `Authen::Passphrase::Argon2`
- `Math::Random::Secure` - `Math::Random::Secure`

View File

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

View File

@ -1,32 +1,17 @@
package CharmBoard; package CharmBoard;
use utf8; use utf8;
use experimental 'try', 'smartmatch'; use strict;
use warnings;
use experimental qw(try smartmatch);
use Mojo::Base 'Mojolicious', -signatures; use Mojo::Base 'Mojolicious', -signatures;
use CharmBoard::Schema; use CharmBoard::Schema;
=pod
=encoding utf8
=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 # this method will run once at server start
sub startup ($self) { sub startup {
my $self = shift;
# load plugins that require no additional conf # load plugins that require no additional conf
$self->plugin('TagHelpers'); $self->plugin('TagHelpers');
@ -55,7 +40,7 @@ sub startup ($self) {
$dsn = "dbi:SQLite:" . $config->{database}->{name}; $dsn = "dbi:SQLite:" . $config->{database}->{name};
$dbUnicode = "sqlite_unicode"} $dbUnicode = "sqlite_unicode"}
elsif ($self->config->{database}->{type} ~~ 'mysql') { elsif ($self->config->{database}->{type} ~~ 'mariadb') {
$dsn = "dbi:mysql:" . $config->{database}->{name}; $dsn = "dbi:mysql:" . $config->{database}->{name};
$dbUnicode = "mysql_enable_utf"} $dbUnicode = "mysql_enable_utf"}
@ -63,7 +48,7 @@ sub startup ($self) {
in charmboard.conf. If you're sure you've set it to in charmboard.conf. If you're sure you've set it to
something supported, maybe double check your spelling? something supported, maybe double check your spelling?
\n\n\t \n\n\t
Valid options: 'sqlite', 'mysql'"}; Valid options: 'sqlite', 'mariadb'"};
my $schema = CharmBoard::Schema->connect( my $schema = CharmBoard::Schema->connect(
$dsn, $dsn,
@ -104,3 +89,5 @@ sub startup ($self) {
} }
1; 1;
__END__

View File

@ -1,11 +1,45 @@
package CharmBoard::Controller::Index; package CharmBoard::Controller::Index;
use utf8; 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 Mojo::Base 'Mojolicious::Controller', -signatures;
use Tree::Simple;
sub index ($self) { sub index {
$self->render(template => '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; 1;
__END__

View File

@ -1,23 +1,22 @@
package CharmBoard::Controller::Login; package CharmBoard::Controller::Login;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8; use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures; use Mojo::Base 'Mojolicious::Controller', -signatures;
use CharmBoard::Crypt::Password; use CharmBoard::Crypt::Password;
use CharmBoard::Crypt::Seasoning; use CharmBoard::Crypt::Seasoning;
=pod sub login {
=encoding utf8 my $self = shift;
=head1 NAME
CharmBoard::Controller::Login
=cut
sub login ($self) {
$self->render( $self->render(
template => 'login', template => 'login',
error => $self->flash('error'), error => $self->flash('error'),
message => $self->flash('message'))}; message => $self->flash('message'))};
sub login_do ($self) { sub login_do {
my $self = shift;
my $username = $self->param('username'); my $username = $self->param('username');
my $password = $self->pepper . ':' . $self->param('password'); my $password = $self->pepper . ':' . $self->param('password');
@ -29,7 +28,7 @@ sub login_do ($self) {
try { # check user credentials first try { # check user credentials first
# check to see if user by entered username exists # check to see if user by entered username exists
$userInfo = $self->schema->resultset('Users')->find( $userInfo = $self->schema->resultset('Users')->search(
{username => $username}); {username => $username});
$userInfo or die; $userInfo or die;
@ -75,3 +74,9 @@ sub login_do ($self) {
$self->redirect_to('login')}} $self->redirect_to('login')}}
1; 1;
__END__
=pod
=head1 NAME
CharmBoard::Controller::Login
=cut

View File

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

View File

@ -1,18 +1,23 @@
package CharmBoard::Controller::Register; package CharmBoard::Controller::Register;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8; use utf8;
use experimental 'try', 'smartmatch';
use Mojo::Base 'Mojolicious::Controller', -signatures; use Mojo::Base 'Mojolicious::Controller', -signatures;
use CharmBoard::Crypt::Password; use CharmBoard::Crypt::Password;
# initial registration page # initial registration page
sub register ($self) { sub register {
my $self = shift;
$self->render( $self->render(
template => 'register', template => 'register',
error => $self->flash('error'), error => $self->flash('error'),
message => $self->flash('message'))}; message => $self->flash('message'))};
# process submitted registration form # process submitted registration form
sub register_do ($self) { sub register_do {
my $self = shift;
my $username = $self->param('username'); my $username = $self->param('username');
my $email = $self->param('email'); my $email = $self->param('email');
my $password = $self->param('password'); my $password = $self->param('password');
@ -39,10 +44,10 @@ sub register_do ($self) {
# check to make sure username and/or email isn't already in use; # check to make sure username and/or email isn't already in use;
# if not, continue with registration # if not, continue with registration
## search for input username and email in database ## search for input username and email in database
$userCheck = $self->schema->resultset('Users')->find( $userCheck = $self->schema->resultset('Users')->search(
{username => $username}); {username => $username})->single;
$emailCheck = $self->schema->resultset('Users')->find( $emailCheck = $self->schema->resultset('Users')->search(
{email => $email}); {email => $email})->single;
($userCheck && $emailCheck) eq undef ($userCheck && $emailCheck) eq undef
or die "Username already in use.\nemail already in use."; or die "Username already in use.\nemail already in use.";

View File

@ -1,4 +1,7 @@
package CharmBoard::Crypt::Password; package CharmBoard::Crypt::Password;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8; use utf8;
use Authen::Passphrase::Argon2; use Authen::Passphrase::Argon2;
use CharmBoard::Crypt::Seasoning; use CharmBoard::Crypt::Seasoning;
@ -6,40 +9,7 @@ use CharmBoard::Crypt::Seasoning;
use Exporter qw(import); use Exporter qw(import);
our @EXPORT = qw(passgen passchk); our @EXPORT = qw(passgen passchk);
=pod sub passgen {
=encoding utf8
=head1 NAME
CharmBoard::Crypt::Password - password processing module
=head1 SYNOPSIS
=begin perl
use CharmBoard::Crypt::Password;
my ($salt, $hash) =
passgen($plaintextPassword);
$passwordVerification =
passchk($salt, $hash, $plaintextPassword)
=end perl
=head1 DESCRIPTION
CharmBoard::Crypt::Password processes passwords, either
processing new passwords for database storage, or checking
passwords entered 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.
=over
=cut
=pod
=item 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( my $argon2 = Authen::Passphrase::Argon2->new(
salt => seasoning(32), salt => seasoning(32),
passphrase => $_[0], passphrase => $_[0],
@ -50,21 +20,7 @@ sub passgen ($) {
return ($argon2->salt_hex, $argon2->hash_hex)}; return ($argon2->salt_hex, $argon2->hash_hex)};
=pod sub passchk {
=item passchk
passchk is the function for checking plaintext passwords
against the hashed password + salt already stored in the
database. It takes the salt and Argon2 hash string in hex
form plus the plaintext password as inputs, and outputs a
true/false value indicating whether or not 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.)
=back
=cut
sub passchk ($$$) {
my $argon2 = Authen::Passphrase::Argon2->new( my $argon2 = Authen::Passphrase::Argon2->new(
salt_hex => $_[0], salt_hex => $_[0],
hash_hex => $_[1], hash_hex => $_[1],
@ -76,3 +32,37 @@ sub passchk ($$$) {
return ($argon2->match($_[2]))} return ($argon2->match($_[2]))}
1; 1;
__END__
=pod
=head1 NAME
CharmBoard::Crypt::Password - password processing module
=head1 SYNOPSIS
=begin perl
use CharmBoard::Crypt::Password;
($salt, $hash) = passgen($plaintextPassword);
$passwordVerification = passchk($salt, $hash, $plaintextPassword)
=end perl
=head1 DESCRIPTION
CharmBoard::Crypt::Password processes passwords, either processing
new passwords for database storage, or checking passwords entered
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.
=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.
=head2 passchk
passchk is the function for checking plaintext passwords against the
hashed password + salt already stored in the database. It takes the
salt and Argon2 hash string in hex form plus the plaintext password
as inputs, and outputs a true/false value indicating whether or not
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

View File

@ -1,15 +1,18 @@
package CharmBoard::Crypt::Seasoning; package CharmBoard::Crypt::Seasoning;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8; use utf8;
use Math::Random::Secure qw(irand); use Math::Random::Secure qw(irand);
use Exporter qw(import); use Exporter qw(import);
our @EXPORT = qw(seasoning); 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 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 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 ! @ $ % ^ G H I J K L M N O P Q R S T U V W X Y Z ! @ $ % ^
& * / ? . ; : \ [ ] - _ < > ` ~ + = £ ¥ ¢); & * / ? . ; : \ [ ] - _ < > ` ~ + = £ ¥ ¢ §);
my $blend; my $blend;
while (length($blend) < $_[0]) { while (length($blend) < $_[0]) {

View File

@ -1,4 +1,8 @@
package CharmBoard::Schema; package CharmBoard::Schema;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Schema); use base qw(DBIx::Class::Schema);
__PACKAGE__->load_namespaces( __PACKAGE__->load_namespaces(
@ -6,3 +10,5 @@ __PACKAGE__->load_namespaces(
resultset_namespace => 'Set'); resultset_namespace => 'Set');
1; 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,53 +1,24 @@
package CharmBoard::Schema::Source::Categories; package CharmBoard::Schema::Source::Categories;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
=pod
=encoding utf8
=head1 NAME
CharmBoard::Schema::Source::Categories - DBIx::Class
ResultSource module for the database's C<categories> table
=head1 DESCRIPTION
This table contains info about categories, which are used
solely to organize subforums on places like the index page.
=head2 Columns
=over
=item cat_id
Contains unique IDs for individual categories.
Data type is SQLite C<INTEGER>; MariaDB C<INT>.
Cannot be C<NULL>.
=item cat_rank
The order in which categories should be displayed.
Data type is SQLite C<INTEGER>; MariaDB C<SMALLINT>.
Cannot be C<NULL>.
=item cat_name
The name of the category to be displayed in the forum list.
Data type is SQLite C<TEXT>; MariaDB C<TINYTEXT>.
Cannot be C<NULL>.
=back
=cut
__PACKAGE__->table('categories'); __PACKAGE__->table('categories');
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
cat_id => { cat_id => {
data_type => 'integer', data_type => 'integer',
is_numeric => 1,
is_auto_increment => 1, is_auto_increment => 1,
is_nullable => 0, }, is_nullable => 0, },
cat_rank => { cat_rank => {
data_type => 'integer', data_type => 'integer',
is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
cat_name => { cat_name => {
data_type => 'text', data_type => 'text',
is_auto_increment => 0,
is_nullable => 0, }); is_nullable => 0, });
__PACKAGE__->set_primary_key('cat_id'); __PACKAGE__->set_primary_key('cat_id');
1 1;
__END__

View File

@ -1,74 +1,26 @@
package CharmBoard::Schema::Source::Posts; package CharmBoard::Schema::Source::Posts;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
=pod
=encoding utf8
=head1 NAME
CharmBoard::Schema::Source::Posts - DBIx::Class
ResultSource module for the database's C<posts> table
=head1 DESCRIPTION
This table contains post content and other important post
information (such as post date).
=head2 Columns
=over
=item post_id
Contains unique IDs for posts.
Data type is SQLite C<INTEGER>; MariaDB C<INT>. Cannot
be C<NULL>.
=item user_id
Contains the user ID of the creator of a given post.
Is foreign key of C<users.user_id>, and as such, shares the
same datatype (SQLite C<INTEGER>; MariaDB C<INT>).
Cannot be C<NULL>.
=item thread_id
Contains the ID of the thread this post was posted in.
Is foreign key of C<threads.thread_id>, and as such, shares
the same datatype (C<INTEGER> in SQLite). Cannot be C<NULL>.
=item post_date
Contains the date the post was made, in the format provided
by Perl's C<time> function.
Data type is SQLite C<INTEGER>. Cannot be C<NULL>.
=item post_body
Contains the actual text content of the post.
Data type is SQLite C<TEXT>. Cannot be C<NULL>.
=back
=cut
__PACKAGE__->table('posts'); __PACKAGE__->table('posts');
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
post_id => { post_id => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 0,
is_auto_increment => 1, is_auto_increment => 1,
is_nullable => 0, }, is_nullable => 0, },
user_id => { user_id => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 1, is_foreign_key => 1,
is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
thread_id => { thread_id => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 1, is_foreign_key => 1,
is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
post_date => { post_date => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 0,
is_auto_increment => 0,
is_nullable => 0, },
post_body => {
data_type => 'text',
is_foreign_key => 0,
is_auto_increment => 0,
is_nullable => 0, }); is_nullable => 0, });
__PACKAGE__->set_primary_key('post_id'); __PACKAGE__->set_primary_key('post_id');
@ -82,4 +34,5 @@ __PACKAGE__->belongs_to(
'CharmBoard::Schema::Source::Threads', 'CharmBoard::Schema::Source::Threads',
'thread_id' ); 'thread_id' );
1 1;
__END__

View File

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

View File

@ -1,47 +1,10 @@
package CharmBoard::Schema::Source::Subforums; package CharmBoard::Schema::Source::Subforums;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
=pod
=encoding utf8
=head1 NAME
CharmBoard::Schema::Source::Subforums - DBIx::Class
ResultSource module for the database's C<subforums> table
=head1 DESCRIPTION
This table contains information about subforums.
=head2 Columns
=over
=item subf_id
Contains unique IDs for individual subforums.
Data type is SQLite C<INTEGER>.. Cannot be C<NULL>.
=item subf_cat
Which category from the C<categories> table this subforum
belongs to and should show up as a child of.
Is foreign key of C<categories.cat_id> and as such, shares
the same datatype (C<INTEGER> in SQLite). Cannot be C<NULL>.
=item subf_rank
The order in which subforums from a given category should
be displayed, which should be unique per category group.
Data type is SQLite C<INTEGER>. Cannot be C<NULL>.
=item subf_name
The name of the subforum to be displayed in the forum list,
or anywhere else where it is relevant.
Data type is SQLite C<TEXT>. Cannot be C<NULL>.
=item subf_desc
A blurb of text describing the use of a subforum to be
displayed in the forum list.
Data type is SQLite C<TEXT>. Can be C<NULL>.
=back
=cut
__PACKAGE__->table('subforums'); __PACKAGE__->table('subforums');
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
subf_id => { subf_id => {
@ -51,20 +14,16 @@ __PACKAGE__->add_columns(
subf_cat => { subf_cat => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 1, is_foreign_key => 1,
is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
subf_rank => { subf_rank => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 0, is_numeric => 1,
is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
subf_name => { subf_name => {
data_type => 'text', data_type => 'text',
is_auto_increment => 0,
is_nullable => 0, }, is_nullable => 0, },
subf_desc => { subf_desc => {
data_type => 'text', data_type => 'text',
is_auto_increment => 0,
is_nullable => 1, }); is_nullable => 1, });
__PACKAGE__->set_primary_key('subf_id'); __PACKAGE__->set_primary_key('subf_id');
@ -74,4 +33,5 @@ __PACKAGE__->belongs_to(
'CharmBoard::Schema::Source::Categories', 'CharmBoard::Schema::Source::Categories',
{'foreign.cat_id' => 'self.subf_cat'}); {'foreign.cat_id' => 'self.subf_cat'});
1 1;
__END__

View File

@ -1,33 +1,10 @@
package CharmBoard::Schema::Source::Threads; package CharmBoard::Schema::Source::Threads;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
=pod
=encoding utf8
=head1 NAME
CharmBoard::Schema::Source::Categories - DBIx::Class
ResultSource module for the database's C<threads> table
=head1 DESCRIPTION
This table contains information about threads.
=head2 COLUMNS
=over
=item thread_id
Contains unique IDs for threads.
Data type is SQLite C<INTEGER>. Cannot be C<NULL>.
=item thread_title
Contains the title for a given thread.
Data type is SQLite C<TEXT>. Cannot be C<NULL>.
=item thread_subf
Contains the ID of the subforum a given thread belongs to.
Is foreign key of C<subforums.subf_id>, and as such, shares
the same datatype (SQLite C<INTEGER>). Cannot be C<NULL>.
=back
=cut
__PACKAGE__->table('threads'); __PACKAGE__->table('threads');
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
thread_id => { thread_id => {
@ -43,9 +20,11 @@ __PACKAGE__->add_columns(
is_nullable => 0, }); is_nullable => 0, });
__PACKAGE__->set_primary_key('thread_id'); __PACKAGE__->set_primary_key('thread_id');
__PACKAGE__->belongs_to( __PACKAGE__->belongs_to(
thread_subf => thread_subf =>
'CharmBoard::Schema::Source::Subforums', 'CharmBoard::Schema::Source::Subforums',
{'foreign.subf_id' => 'self.thread_subf'}); {'foreign.subf_id' => 'self.thread_subf'});
1 1;
__END__

View File

@ -1,53 +1,10 @@
package CharmBoard::Schema::Source::Users; package CharmBoard::Schema::Source::Users;
use strict;
use warnings;
use experimental qw(try smartmatch);
use utf8; use utf8;
use base qw(DBIx::Class::Core); use base qw(DBIx::Class::Core);
=pod
=encoding utf8
=head1 NAME
CharmBoard::Schema::Source::Users - DBIx::Class
ResultSource module for the database's C<users> table
=head1 DESCRIPTION
This table contains information about users.
=head2 COLUMNS
=over
=item user_id
Contains unique IDs for users.
Data type is SQLite C<INTEGER>; MariaDB C<INT>. Cannot
be C<NULL>.
=item username
Contains a given user's username. Please do not use this
field as an identifier for users, that's what C<user_id> is
intended for, instead.
Data type is SQLite C<TEXT>. Cannot be C<NULL>.
=item email
Contains a user's email address.
Data type is SQLite C<TEXT>. Cannot be C<NULL>.
=item password
Contains the user's password in the form of a
Crypt::Passphrase string in hexadecimal form.
Data type is SQLite C<TEXT>. Cannot be C<NULL>.
=item salt
Contains the user's salt in hexadecimal form.
Data type is SQLite C<TEXT>. Cannot be C<NULL>.
=item signup_date
Contains the date the user signed up, in the format
provided by Perl's C<time> function.
Data type is SQLite C<INTEGER>. Cannot be C<NULL>.
=back
=cut
__PACKAGE__->table('users'); __PACKAGE__->table('users');
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
user_id => { user_id => {
@ -57,19 +14,15 @@ __PACKAGE__->add_columns(
is_auto_increment => 1, }, is_auto_increment => 1, },
username => { username => {
data_type => 'text', data_type => 'text',
is_numeric => 0,
is_nullable => 0, }, is_nullable => 0, },
email => { email => {
data_type => 'text', data_type => 'text',
is_numeric => 0,
is_nullable => 0, }, is_nullable => 0, },
password => { password => {
data_type => 'text', data_type => 'text',
is_numeric => 0,
is_nullable => 0, }, is_nullable => 0, },
salt => { salt => {
data_type => 'text', data_type => 'text',
is_numeric => 0,
is_nullable => 0, }, is_nullable => 0, },
signup_date => { signup_date => {
data_type => 'integer', data_type => 'integer',
@ -78,4 +31,5 @@ __PACKAGE__->add_columns(
__PACKAGE__->set_primary_key('user_id'); __PACKAGE__->set_primary_key('user_id');
1 1;
__END__

View File

@ -1,5 +1,5 @@
#!/usr/bin/env perl #!/usr/bin/env perl
use experimental 'try', 'smartmatch'; use experimental qw(try smartmatch);
use strict; use strict;
use warnings; use warnings;
use utf8; use utf8;
@ -10,3 +10,28 @@ use Mojolicious::Commands;
# Start command line interface for application # Start command line interface for application
Mojolicious::Commands->start_app('CharmBoard'); 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; % 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)) %>
<% }} %>