summaryrefslogtreecommitdiff
path: root/lib/JWebmail/Plugin/Helper.pm
diff options
context:
space:
mode:
authorJannis M. Hoffmann <jannis.hoffmann@rwth-aachen.de>2020-10-29 12:13:04 +0100
committerJannis M. Hoffmann <jannis.hoffmann@rwth-aachen.de>2020-10-29 12:13:04 +0100
commitee43823179ee627ac16ea9da8168e5f1bf9619c0 (patch)
tree5e6c36d5629d2ce79f3cb1310998dc715a6f19c7 /lib/JWebmail/Plugin/Helper.pm
Initial commit; Stable version
Diffstat (limited to 'lib/JWebmail/Plugin/Helper.pm')
-rw-r--r--lib/JWebmail/Plugin/Helper.pm448
1 files changed, 448 insertions, 0 deletions
diff --git a/lib/JWebmail/Plugin/Helper.pm b/lib/JWebmail/Plugin/Helper.pm
new file mode 100644
index 0000000..5e557d1
--- /dev/null
+++ b/lib/JWebmail/Plugin/Helper.pm
@@ -0,0 +1,448 @@
+package JWebmail::Plugin::Helper;
+
+use Mojo::Base 'Mojolicious::Plugin';
+
+use POSIX qw(floor round log ceil);
+use MIME::Base64;
+use Encode;
+use Mojo::Util 'xml_escape';
+use List::Util qw(min max);
+
+use constant TRUE_RANDOM => eval { require Crypt::Random; Crypt::Random->import('makerandom_octet'); 1 };
+use constant HMAC => eval { require Digest::HMAC_MD5; Digest::HMAC_MD5->import('hmac_md5'); 1 };
+
+### filter and checks for mojo validator
+
+sub mail_line {
+ my ($v, $name, $value, @args) = @_;
+
+ my $mail_addr = qr/\w+\@\w+\.\w+/;
+ # my $unescaped_quote = qr/"(*nlb:\\)/; # greater perl version required
+ my $unescaped_quote = qr/"(?<!:\\)/;
+
+ return $value !~ /^(
+ (
+ (
+ (
+ $unescaped_quote.*?$unescaped_quote
+ ) | (
+ [\w\s]*
+ )
+ )
+ \s*<$mail_addr>
+ ) | (
+ $mail_addr
+ ))$
+ /xno;
+}
+
+
+sub filter_empty_upload {
+ my ($v, $name, $value) = @_;
+
+ return $value->filename ? $value : undef;
+}
+
+### template formatting functions
+
+sub print_sizes10 {
+ my $var = shift;
+ if ($var == 0) { return '0 Byte'; }
+
+ my $i = floor(((log($var)/log(10))+1e-5) / 3);
+ my $expo = $i * 3;
+
+ my @PREFIX;
+ $PREFIX[0] = 'Byte';
+ $PREFIX[1] = 'kByte';
+ $PREFIX[2] = 'MByte';
+ $PREFIX[3] = 'GByte';
+ $PREFIX[4] = 'TByte';
+ $PREFIX[5] = 'PByte';
+
+ return sprintf('%.0f %s', $var / (10**$expo), $PREFIX[$i]);
+}
+
+
+sub print_sizes2 {
+ my $var = shift;
+ if ($var == 0) { return '0 Byte'; }
+
+ my $i = floor(((log($var)/log(2))+1e-5) / 10);
+ my $expo = $i * 10;
+ my %PREFIX = (
+ 0 => 'Byte',
+ 1 => 'KiByte',
+ 2 => 'MiByte',
+ 3 => 'GiByte',
+ 4 => 'TiByte',
+ 5 => 'PiByte',
+ );
+ my $pref = $PREFIX{$i};
+ return round($var / (2**$expo)) . " $pref";
+}
+
+### mime type html render functions
+
+my $render_text_plain = sub {
+ my ($c, $content) = @_;
+
+ $content = xml_escape($content);
+ $content =~ s/\n/<br>/g;
+
+ return $content;
+};
+
+
+my $render_text_html = sub {
+ my $c_ = shift;
+
+ return '<iframe src="' . $c_->url_for('rawid', id => $c_->stash('id'))->query(body => 'html') . '" class=html-mail />';
+};
+
+
+our %MIME_Render_Subs = (
+ 'text/plain' => $render_text_plain,
+ 'text/html' => $render_text_html,
+);
+
+
+sub mime_render {
+ my ($c, $enc, $cont) = @_;
+
+ my $renderer = $MIME_Render_Subs{$enc};
+ return '' unless defined $renderer;
+ return $renderer->($c, $cont);
+};
+
+### session password handling
+
+use constant { S_PASSWD => 'pw', S_OTP_S3D_PW => 'otp_s3d_pw' };
+
+sub _rand_data {
+ my $len = shift;
+
+ return makerandom_octet(Length => $len, Strength => 0);
+}
+
+sub _pseudo_rand_data {
+ my $len = shift;
+
+ my $res = '';
+ for (0..$len-1) {
+ vec($res, $_, 8) = int rand 256;
+ }
+
+ return $res;
+}
+
+sub session_passwd {
+ my ($c, $passwd) = @_;
+
+ warn_cram($c);
+ warn_crypt($c);
+
+ if (defined $passwd) { # set
+ if ( HMAC && lc($c->config->{'session'}{secure} || 'none') eq 'cram' ) {
+ $c->session(S_PASSWD() => $passwd ? encode_base64(hmac_md5($passwd, $c->app->secrets->[0]), '') : '');
+ }
+ elsif (lc($c->config->{'session'}->{secure} || 'none') eq 's3d') {
+ unless ($passwd) {
+ $c->s3d(S_PASSWD, '');
+ delete $c->session->{S_OTP_S3D_PW()};
+ return;
+ }
+ die "'$passwd' contains invalid character \\n" if $passwd =~ /\n/;
+ if (length $passwd < 20) {
+ $passwd .= "\n" . " " x (20 - length($passwd) - 1);
+ }
+ my $rand_bytes = TRUE_RANDOM ? _rand_data(length $passwd) : _pseudo_rand_data(length $passwd);
+ $c->s3d(S_PASSWD, encode_base64(encode('UTF-8', $passwd) ^ $rand_bytes, ''));
+ $c->session(S_OTP_S3D_PW, encode_base64($rand_bytes, ''));
+ }
+ else {
+ $c->session(S_PASSWD() => $passwd);
+ }
+ }
+ else { # get
+ if ( HMAC && lc($c->config->{'session'}->{secure} || 'none') eq 'cram' ) {
+ return ($c->app->secrets->[0], $c->session(S_PASSWD));
+ }
+ elsif (lc($c->config->{'session'}->{secure} || 'none') eq 's3d') {
+ my $pw = decode_base64($c->s3d(S_PASSWD) || '');
+ my $otp = decode_base64($c->session(S_OTP_S3D_PW) || '');
+ my ($res) = split "\n", decode('UTF-8', $pw ^ $otp), 2;
+ return $res;
+ }
+ else {
+ return $c->session(S_PASSWD);
+ }
+ }
+}
+
+sub warn_cram {
+ my $c = shift;
+
+ state $once = 0;
+
+ if ( !HMAC && !$once && lc($c->config->{'session'}->{secure} || 'none') eq 'cram' ) {
+ $c->log->warn("cram requires Digest::HMAC_MD5. Falling back to 'none'.");
+ }
+
+ $once = 1;
+}
+
+sub warn_crypt {
+ my $c = shift;
+
+ state $once = 0;
+
+ if ( !TRUE_RANDOM && !$once && lc($c->config->{'session'}->{secure} || 'none') eq 's3d' ) {
+ $c->log->warn("Falling back to pseudo random generation. Please install Crypt::Random");
+ }
+
+ $once = 1;
+}
+
+### pagination
+
+sub _clamp {
+ my ($x, $y, $z) = @_;
+
+ die '!($x <= $z)' unless $x <= $z;
+
+ if ($x <= $y && $y <= $z) {
+ return $y;
+ }
+
+ return $x if ($y < $x);
+ return $z if ($z < $y);
+}
+
+sub _paginate {
+ my %args = @_;
+
+ my $first_item = $args{first_item};
+ my $page_size = $args{page_size} || 1;
+ my $total_items = $args{total_items};
+
+ my $first_item1 = $total_items ? $first_item+1 : 0;
+
+ my $current_page = ceil($first_item/$page_size);
+ my $total_pages = ceil($total_items/$page_size);
+
+ my $page = sub {
+ my $page_ = shift;
+ return [0, 0] unless $total_items;
+ $page_ = _clamp(0, $page_, $total_pages-1);
+ [_clamp(1, $page_*$page_size + 1, $total_items), _clamp(1, ($page_+1)*$page_size, $total_items)]
+ };
+
+ return (
+ first_item => $first_item1,
+ last_item => _clamp($first_item1, $first_item + $page_size, $total_items),
+ total_items => $total_items,
+ page_size => $page_size,
+
+ total_pages => $total_pages,
+ current_page => $current_page + 1,
+
+ first_page => $page->(0),
+ prev_page => $page->($current_page-1),
+ next_page => $page->($current_page+1),
+ last_page => $page->($total_pages-1),
+ );
+}
+
+sub paginate {
+ my $c = shift;
+ my $count = shift;
+
+ my $v = $c->validation;
+ my $start = $v->optional('start')->num(0, undef)->param // 0;
+ my $psize = $v->optional('page_size')->num(1, undef)->param // 50;
+
+ $start = _clamp(0, $start, max($count-1, 0));
+ my $end = _clamp($start, $start+$psize-1, max($count-1, 0));
+
+ $c->stash(_paginate(first_item => $start, page_size => $psize, total_items => $count));
+
+ return $start, $end;
+}
+
+### registering
+
+sub register {
+ my ($self, $app, $conf) = @_;
+
+ if (ref $conf->{import} eq 'ARRAY' and my @import = @{ $conf->{import} }) {
+ no warnings 'experimental::smartmatch';
+
+ # selective import
+ $app->helper(print_sizes10 => sub { shift; print_sizes10(@_) })
+ if 'print_sizes10' ~~ @import;
+ $app->helper(print_sizes2 => sub { shift; print_sizes2(@_) })
+ if 'print_sizes2' ~~ @import;
+ $app->helper(mime_render => \&mime_render)
+ if 'mime_render' ~~ @import;
+ $app->helper(session_passwd => \&session_passwd)
+ if 'session_passwd' ~~ @import;
+ $app->helper(paginate => \&paginate)
+ if 'paginate' ~~ @import;
+ $app->validator->add_check(mail_line => \&mail_line)
+ if 'mail_line' ~~ @import;
+ $app->validator->add_filter(non_empty_ul => \&filter_empty_upload)
+ if 'non_empty_ul' ~~ @import;
+ }
+ elsif (!$conf->{import}) { # default imports
+ $app->helper(print_sizes10 => sub { shift; print_sizes10(@_) });
+ $app->helper(mime_render => \&mime_render);
+ $app->helper(session_passwd => \&session_passwd);
+ $app->helper(paginate => \&paginate);
+
+ $app->validator->add_check(mail_line => \&mail_line);
+
+ $app->validator->add_filter(non_empty_ul => \&filter_empty_upload);
+ }
+}
+
+
+1
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+Helper - Functions used as helpers in controller and templates and additional validator checks and filters
+
+=head1 SYNOPSIS
+
+ use Mojo::Base 'Mojolicious';
+
+ use JWebmail::Plugin::Helper;
+
+ sub startup($self) {
+ $self->helper(mime_render => \&JWebmail::Plugin::Helper::mime_render);
+ }
+
+ # or
+
+ $app->plugin('Helper');
+
+=head1 DESCRIPTION
+
+L<JWebmail::Helper> provides useful helper functions and validator cheks and filter for
+L<JWebmail::Controller::All> and various templates.
+
+=head1 FUNCTIONS
+
+=head2 mail_line
+
+A check for validator used in mail headers for fields containing email addresses.
+
+ $app->validator->add_check(mail_line => \&JWebmail::Plugin::Helper::mail_line);
+
+ my $v = $c->validation;
+ $v->required('to', 'not_empty')->check('mail_line');
+
+=head2 filter_empty_upload
+
+A filter for validator used to filter out empty uploads.
+
+ $app->validator->add_filter(non_empty_ul => \&JWebmail::Plugin::Helper::filter_empty_upload);
+
+ my $v = $c->validation;
+ $v->required('file_upload', 'non_empty_ul');
+
+=head2 print_sizes10
+
+A helper for templates used to format byte sizes.
+
+ $app->helper(print_sizes10 => sub { shift; JWebmail::Plugin::Helper::print_sizes10(@_) });
+
+ %= print_sizes10 12345 # => 12 kB
+
+=head2 print_sizes2
+
+A helper for templates used to format byte sizes.
+
+ %= print_sizes10 12345 # => 12 KiB
+
+This is not registered by default.
+
+=head2 paginate
+
+A helper for calculationg page bounds.
+
+Takes the total number of items as argument.
+
+Reads in 'start' and 'page_size' query arguments.
+start is 0 based.
+
+Returns the calculated start and end points as 0 based inclusive range.
+
+Sets the stash values (all 1 based inclusive):
+
+ first_item
+ last_item
+ total_items
+ page_size
+ total_pages
+ current_page
+ first_page
+ prev_page
+ next_page
+ last_page
+
+=head2 mime_render
+
+A helper for templates used to display the content of a mail for the browser.
+The output is valid html and should not be escaped.
+
+ $app->helper(mime_render => \&JWebmail::Plugin::Helper::mime_render);
+
+ %== mime_render 'text/plain' $content
+
+=head2 session_passwd
+
+A helper used to set and get the session password. The behaivour can be altered by
+setting the config variable C<< session => {secure => 's3d'} >>.
+
+ $app->helper(session_passwd => \&JWebmail::Plugin::Helper::session_passwd);
+
+ $c->session_passwd('s3cret');
+
+Currently the following modes are supported:
+
+=over 6
+
+=item none
+
+password is plainly stored in session cookie
+
+=item cram
+
+challenge response authentication mechanism uses the C<< $app->secret->[0] >> as nonce.
+This is optional if Digest::HMAC_MD5 is installed.
+
+=item s3d
+
+data is stored on the server. Additionally the password is encrypted by an one-time-pad that is stored in the user cookie.
+
+=back
+
+=head1 DEPENDENCIES
+
+Mojolicious, Crypt::Random and optianally Digest::HMAC_MD5.
+
+=head1 SEE ALSO
+
+L<JWebmail>, L<JWebmail::Controller::All>, L<Mojolicious>, L<Mojolicious::Controller>
+
+=head1 NOTICE
+
+This package is part of JWebmail.
+
+=cut \ No newline at end of file