diff options
author | Jannis M. Hoffmann <jannis.hoffmann@rwth-aachen.de> | 2020-10-29 12:13:04 +0100 |
---|---|---|
committer | Jannis M. Hoffmann <jannis.hoffmann@rwth-aachen.de> | 2020-10-29 12:13:04 +0100 |
commit | ee43823179ee627ac16ea9da8168e5f1bf9619c0 (patch) | |
tree | 5e6c36d5629d2ce79f3cb1310998dc715a6f19c7 /lib/JWebmail/Plugin |
Initial commit; Stable version
Diffstat (limited to 'lib/JWebmail/Plugin')
-rw-r--r-- | lib/JWebmail/Plugin/Helper.pm | 448 | ||||
-rw-r--r-- | lib/JWebmail/Plugin/I18N.pm | 212 | ||||
-rw-r--r-- | lib/JWebmail/Plugin/I18N2.pm | 185 | ||||
-rw-r--r-- | lib/JWebmail/Plugin/INIConfig.pm | 136 | ||||
-rw-r--r-- | lib/JWebmail/Plugin/ServerSideSessionData.pm | 147 |
5 files changed, 1128 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 diff --git a/lib/JWebmail/Plugin/I18N.pm b/lib/JWebmail/Plugin/I18N.pm new file mode 100644 index 0000000..dc10fdd --- /dev/null +++ b/lib/JWebmail/Plugin/I18N.pm @@ -0,0 +1,212 @@ +package JWebmail::Plugin::I18N; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Mojolicious::Controller; +use Mojo::File; +use Mojo::Util 'monkey_patch'; + + +has '_language_loaded' => sub { {} }; + + +sub register { + my ($self, $app, $conf) = @_; + + my $i18n_log = $app->log->context('[' . __PACKAGE__ . ']'); + + # config + # 1. what languages + # 2. where are the files + # 3. fallback language + # + # look for languages automatically + my $defaultLang = $conf->{default_language} || 'en'; + my $fileLocation = $conf->{directory} && Mojo::File->new($conf->{directory})->is_abs + ? $conf->{directory} + : $app->home->child($conf->{directory} || 'lang'); + my @languages = keys %{$conf->{languages} // {}}; + + unless (@languages) { + @languages = map { $_ =~ s|^.*/(..)\.lang$|$1|r } glob("$fileLocation/*.lang"); + } + + $app->defaults(lang => $defaultLang); + $app->defaults(languages => [@languages]); + + # load languages + my $TXT; + for my $l (@languages) { + $TXT->{$l} = _loadi18n($fileLocation, $l, $i18n_log); + } + + { + local $" = ','; + $i18n_log->debug("loaded languages (@languages)"); + } + + $self->_language_loaded( { map { $_ => 1 } @languages } ); + + # add translator as helper + my $i18n = sub { + my ($lang, $word) = @_; + $TXT->{$lang}{$word} || scalar( + local $" = ' ', + $lang && $word ? $app->log->debug('[' . __PACKAGE__ . "] missing translation for $lang:$word @{[ caller(2) ]}[0..2]") : (), + '', + ) + }; + $app->helper( l => sub { my $c = shift; $i18n->($c->stash->{lang}, shift) } ); + + # rewrite url + $app->hook(before_dispatch => sub { $self->read_language_hook(@_) }); + + # patch url_for + my $mojo_url_for = Mojolicious::Controller->can('url_for'); + my $i18n_url_for = sub { + my $c = shift; + my $url = $mojo_url_for->($c, @_); + + my $args = (ref $_[0] eq 'HASH' and $_[0]) || (ref $_[1] eq 'HASH' and $_[1]) || do { my %x = @_[(@_ % 2) .. $#_]; \%x }; + my $lang = $args->{lang} // $c->stash->{lang}; + + if ( $lang && (ref $_[0] eq 'HASH' || !ref $_[0] && ($_[0]//'') !~ m![:@/.]!) ) { + unshift @{ $url->path->parts }, $lang + if ($url->path->parts->[0] // '') ne $lang; + $url = $url->to_abs(Mojo::URL->new('/')); + } + + return $url; + }; + monkey_patch 'Mojolicious::Controller', url_for => $i18n_url_for; + + 0 +} + + +sub read_language_hook { + my $self = shift; + my $c = shift; + + # URL detection + if (my $path = $c->req->url->path) { + + my $part = $path->parts->[0]; + + if ( $part && $self->_language_loaded->{$part} ) { + # Ignore static files + return if $c->res->code; + + $c->app->log->debug('[' . __PACKAGE__ . "] Found language $part in URL $path"); + + # Save lang in stash + $c->stash(lang => $part); + + if ( @{ $path->parts } == 1 && !$path->trailing_slash ) { + return $c->redirect_to($c->req->url->path->trailing_slash(1)); # default controller adds language back + } + + # Clean path + shift @{$path->parts}; + $path->trailing_slash(0); + } + } +} + + +sub _loadi18n { + + my $langsubdir = shift; + my $lang = shift; + my $log = shift; + + my $langFile = "$langsubdir/$lang.lang"; + my $TXT; + + if ( -f $langFile ) { + $TXT = Config::Tiny->read($langFile, 'utf8')->{'_'}; + if ($@ || !defined $TXT) { + $log->error("error reading file $langFile: $@"); + } + } + else { + $log->warn("language file $langFile does not exist!"); + } + return $TXT; +} + + +1 + +__END__ + +=encoding utf8 + +=head1 NAME + +JWebmail::Plugin::I18N - Custom Made I18N Support Inspired by Mojolicious::Plugin::I18N + +=head1 SYNOPSIS + + $app->plugin('I18N', { + languages => [qw(en de es)], + default_language => 'en', + directory => '/path/to/language/files/', + }) + + # in your controller + $c->l('hello') + + # in your templates + <%= l 'hello' %> + + @@ de.lang + login = anmelden + userid = nuzerkennung + passwd = passwort + failed = fehlgeschlagen + about = über + + example.com/de/myroute # $c->stash('lang') eq 'de' + example.com/myroute # $c->stash('lang') eq $defaultLanguage + + # on example.com/de/myroute + url_for('my_other_route') #=> example.com/de/my_other_route + + url_for('my_other_route', lang => 'es') #=> example.com/es/my_other_route + +=head1 DESCRIPTION + +L<JWebmail::Plugin::I18N> provides I18N support. + +The language will be taken from the first path segment of the url. +Be carefult with colliding routes. + +Mojolicious::Controller::url_for is patched so that the current language will be kept for +router named urls. + +=head1 OPTIONS + +=head2 default_language + +The default language when no other information is provided. + +=head2 directory + +Directory to look for language files. + +=head2 languages + +List of allowed languages. +Files of the pattern "$lang.lang" will be looked for. + +=head1 HELPERS + +=head2 l + +This is used for your translations. + + $c->l('hello') + $app->helper('hello')->() + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Plugin/I18N2.pm b/lib/JWebmail/Plugin/I18N2.pm new file mode 100644 index 0000000..53813de --- /dev/null +++ b/lib/JWebmail/Plugin/I18N2.pm @@ -0,0 +1,185 @@ +package JWebmail::Plugin::I18N2; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Mojolicious::Controller; +use Mojo::File; +use Mojo::Util 'monkey_patch'; + + +has '_language_loaded' => sub { {} }; + + +sub register { + my ($self, $app, $conf) = @_; + + my $i18n_log = $app->log->context('[' . __PACKAGE__ . ']'); + + # config + # 1. what languages + # 2. where are the files + # 3. fallback language + # + # look for languages automatically + my $defaultLang = $conf->{default_language} || 'en'; + my $fileLocation = $conf->{directory} && Mojo::File->new($conf->{directory})->is_abs + ? $conf->{directory} + : $app->home->child($conf->{directory} || 'lang'); + my @languages = keys %{$conf->{languages} // {}}; + + unless (@languages) { + @languages = map { $_ =~ s|^.*/(..)\.lang$|$1|r } glob("$fileLocation/*.lang"); + } + + $app->defaults(languages => [@languages]); + + # load languages + my $TXT; + for my $l (@languages) { + $TXT->{$l} = _loadi18n($fileLocation, $l, $i18n_log); + } + + { + local $" = ','; + $i18n_log->debug("loaded languages (@languages)"); + } + + $self->_language_loaded( { map { $_ => 1 } @languages } ); + + # add translator as helper + my $i18n = sub { + my ($lang, $word) = @_; + $TXT->{$lang}{$word} || scalar( + local $" = ' ', + $lang && $word ? $app->log->debug('[' . __PACKAGE__ . "] missing translation for $lang:$word @{[ caller(2) ]}[0..2]") : (), + '', + ) + }; + $app->helper( l => sub { my $c = shift; $i18n->($c->stash->{lang}, shift) } ); + + $app->hook(before_dispatch => sub { + my $c = shift; + unshift @{ $c->req->url->path->parts }, '' + unless $self->_language_loaded->{$c->req->url->path->parts->[0] || ''}; + }); + + # patch url_for + my $mojo_url_for = Mojolicious::Controller->can('url_for'); + my $i18n_url_for = sub { + my $c = shift; + if (ref $_[0] eq 'HASH') { + $_[0]->{lang} ||= $c->stash('lang'); + } + elsif (ref $_[1] eq 'HASH') { + $_[1]->{lang} ||= $c->stash('lang'); + } + elsif (@_) { + push @_, lang => $c->stash('lang'); + } + else { + @_ = {lang => $c->stash('lang')}; + } + return $mojo_url_for->($c, @_); + }; + monkey_patch 'Mojolicious::Controller', url_for => $i18n_url_for; + + return $app->routes->any('/:lang' => {lang => 'en'}); +} + + +sub _loadi18n { + + my $langsubdir = shift; + my $lang = shift; + my $log = shift; + + my $langFile = "$langsubdir/$lang.lang"; + my $TXT; + + if ( -f $langFile ) { + $TXT = Config::Tiny->read($langFile, 'utf8')->{'_'}; + if ($@ || !defined $TXT) { + $log->error("error reading file $langFile: $@"); + } + } + else { + $log->warn("language file $langFile does not exist!"); + } + return $TXT; +} + + +1 + +__END__ + +=encoding utf8 + +=head1 NAME + +JWebmail::Plugin::I18N2 - Custom Made I18N Support an alternative to JWebmail::Plugin::I18N + +=head1 SYNOPSIS + + $app->plugin('I18N2', { + languages => [qw(en de es)], + default_language => 'en', + directory => '/path/to/language/files/', + }) + + # in your controller + $c->l('hello') + + # in your templates + <%= l 'hello' %> + + @@ de.lang + login = anmelden + userid = nuzerkennung + passwd = passwort + failed = fehlgeschlagen + about = über + + example.com/de/myroute # $c->stash('lang') eq 'de' + example.com/myroute # $c->stash('lang') eq $defaultLanguage + + # on example.com/de/myroute + url_for('my_other_route') #=> example.com/de/my_other_route + + url_for('my_other_route', lang => 'es') #=> example.com/es/my_other_route + +=head1 DESCRIPTION + +L<JWebmail::Plugin::I18N2> provides I18N support. + +The language will be taken from the first path segment of the url. +Be carefult with colliding routes. + +Mojolicious::Controller::url_for is patched so that the current language will be kept for +router named urls. + +=head1 OPTIONS + +=head2 default_language + +The default language when no other information is provided. + +=head2 directory + +Directory to look for language files. + +=head2 languages + +List of allowed languages. +Files of the pattern "$lang.lang" will be looked for. + +=head1 HELPERS + +=head2 l + +This is used for your translations. + + $c->l('hello') + $app->helper('hello')->() + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Plugin/INIConfig.pm b/lib/JWebmail/Plugin/INIConfig.pm new file mode 100644 index 0000000..fe0fb1a --- /dev/null +++ b/lib/JWebmail/Plugin/INIConfig.pm @@ -0,0 +1,136 @@ +package JWebmail::Plugin::INIConfig; +use Mojo::Base 'Mojolicious::Plugin::Config'; + +use List::Util 'all'; + +use Config::Tiny; + + +sub parse { + my ($self, $content, $file, $conf, $app) = @_; + + my $ct = Config::Tiny->new; + my $config = $ct->read_string($content, 'utf8'); + die qq{Can't parse config "$file": } . $ct->errstr unless defined $config; + + $config = _process_config($config) unless $conf->{flat}; + + return $config; +} + + +sub _process_config { + my $val_prev = shift; + my %val = %$val_prev; + + # arrayify section with number keys + for my $key (keys %val) { + if (keys %{$val{$key}} && all { $_ =~ /\d+/} keys %{$val{$key}}) { + my $tmp = $val{$key}; + $val{$key} = []; + + for (keys %$tmp) { + $val{$key}[$_] = $tmp->{$_}; + } + } + } + + # merge top section + my $top_section = $val{'_'}; + delete $val{'_'}; + for (keys %$top_section) { + $val{$_} = $top_section->{$_} unless $val{$_}; + } + + # make implicit nesting explicit + for my $key (grep { $_ =~ /^\w+(::\w+)+$/} keys %val) { + + my @sections = split m/::/, $key; + my $x = \%val; + my $y; + for (@sections) { + $x->{$_} = {} unless ref $x->{$_};# eq 'HASH'; + $y = $x; + $x = $x->{$_}; + } + # merge + if (ref $val{$key} eq 'ARRAY') { + $y->{$sections[-1]} = []; + $x = $y->{$sections[-1]}; + for ( keys @{ $val{$key} } ) { + $x->[$_] = $val{$key}[$_]; + } + } + else { + for ( keys %{ $val{$key} } ) { + $x->{$_} = $val{$key}{$_}; + } + } + delete $val{$key}; + } + + return \%val +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +INIConfig - Reads in ini config files. + +=head1 SYNOPSIS + + $app->plugin('INIConfig'); + + @@ my_app.conf + + # global section + key = val ; line comment + [section] + other_key = other_val + [other::section] + 0 = key1 + 1 = key2 + 2 = key3 + +=head1 DESCRIPTION + +INI configuration is simple with limited nesting and propper comments. +For more precise specification on the syntax see the Config::Tiny documentation +on metacpan. + +=head1 OPTIONS + +=head2 default + +Sets default configuration values. + +=head2 ext + +Sets file extension defaults to '.conf'. + +=head2 file + +Sets file name default '$app->moniker'. + +=head2 flat + +Keep configuration to exactly two nesting levels for all +and disable auto array conversion. + +=head1 METHODS + +=head2 parse + +overrides the parse method of Mojolicious::Plugin::Config + +=head1 DEPENDENCIES + +Config::Tiny + +=cut
\ No newline at end of file diff --git a/lib/JWebmail/Plugin/ServerSideSessionData.pm b/lib/JWebmail/Plugin/ServerSideSessionData.pm new file mode 100644 index 0000000..9890358 --- /dev/null +++ b/lib/JWebmail/Plugin/ServerSideSessionData.pm @@ -0,0 +1,147 @@ +package JWebmail::Plugin::ServerSideSessionData; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Mojo::JSON qw(decode_json encode_json); +use Mojo::File; + +use constant { + S_KEY => 's3d.key', +}; + + +has '_session_directory'; +sub session_directory { my $self = shift; @_ ? $self->_session_directory(Mojo::File->new(@_)) : $self->_session_directory } + +has 'expiration'; +has 'cleanup_interval'; + +has '_cleanup'; +sub cleanup { + my $self = shift; + if (@_) { + return $self->_cleanup(@_); + } + else { + if ($self->_cleanup < time) { + return 0; + } + else { + $self->_cleanup(time + $self->cleanup_interval); + return 1; + } + } +} + + +sub s3d { + my $self = shift; + my $c = shift; + + # cleanup old sessions + if ($self->cleanup) { + my $t = time; + for ($self->session_directory->list->each) { + if ( $_->stat->mtime + $self->expiration < $t ) { + $_->remove; + } + } + } + + my $file = $self->session_directory->child($c->session(S_KEY) || $c->req->request_id . $$); + + if (-e $file) { + if ($file->stat->mtime + $self->expiration < time) { + $file->remove; + } + else { + $file->touch; + } + } + my $data = decode_json($file->slurp) if (-s $file); + + my ($key, $val) = @_; + + if (defined $val) { # set + unless (-e $file) { + $c->session(S_KEY, $file->basename); + } + $data = ref $data ? $data : {}; + $data->{$key} = $val; + + #$file->spurt(encode_json $data); + open(my $f, '>', $file) or die "$!"; + chmod 0600, $f; + $f->say(encode_json $data); + close($f); + } + else { # get + return defined $key ? $data->{$key} : $data; + } +}; + + +sub register { + my ($self, $app, $conf) = @_; + + $self->session_directory($conf->{directory} || "/tmp/" . $app->moniker); + $self->expiration($conf->{expiration} || $app->sessions->default_expiration); + $self->cleanup_interval($conf->{cleanup_interval} || $self->expiration); + $self->cleanup(time + $self->cleanup_interval); + + unless (-d $self->session_directory) { + mkdir($self->session_directory) + or $! ? die "failed to create directory: $!" : 1; + } + + $app->helper( s3d => sub { $self->s3d(@_) } ); +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +ServeSideSessionData - Stores session data on the server (alias SSSD or S3D) + +=head1 SYNOPSIS + + $app->plugin('ServeSideSessionData'); + + $c->s3d(data => 'Hello, S3D'); + $c->s3d('data'); + +=head1 DESCRIPTION + +Store data temporarily on the server. +The only protetction on the server are struct user access rights. + +=head1 OPTIONS + +=head2 directory + +default C<< 'tmp/' . $app->moniker >> + +=head2 expiration + +default session expiration + +=head2 cleanup_interval + +default session expiration + +=head1 HELPERS + +=head2 s3d + +Stores and retrieves values. + + $c->s3d(data => 'Hello, S3D'); + $c->s3d('data'); + $c->s3d->{data}; + +=cut
\ No newline at end of file |