App-ZofCMS

 view release on metacpan or  search on metacpan

lib/App/ZofCMS/Plugin/UserLogin.pm  view on Meta::CPAN

        if ( $opts{redirect_on_restricted} ) {
            print $config->cgi->redirect(
                $opts{redirect_on_restricted}
                . process_smart_deny( \%opts )
            );
            exit;
        }
    }
    if ( $user_ref ) {
        if ( exists $query{zofcms_plugin_login}
            and $query{zofcms_plugin_login} eq 'logout_user' ) {
            $self->log_user_out( $user_ref );
            if ( $opts{redirect_on_logout} ) {
                print $config->cgi->redirect( $opts{redirect_on_logout} );
                exit;
            }
        }
        else {
            my $t = HTML::Template->new_scalar_ref(
                \ $self->logout_form_template()
            );
            $t->param(
                page            => $query{page},
                logout_button   => $opts{logout_button},
            );
            $template->{t}{plug_login_logout} = $t->output;
        }

        $template->{t}{plug_login_user} = $user_ref->{login};
    }

    return 1;
}

sub log_user_out {
    my ( $self, $user_ref ) = @_;
    my $opts = $self->opts;

    my $dbh = DBI->connect_cached(
        @$opts{ qw/dsn user pass opt/ },
    );

    $dbh->do(
        "UPDATE $opts->{table} SET session_id = ?, login_time = ? WHERE "
        . "login = ?; ",
        undef,
        0,
        0,
        $user_ref->{login},
    );
}

sub is_page_restricted {
    my ( $self, $page, $user_ref ) = @_;
    my $opts = $self->opts;

    if ( $page eq $opts->{login_page} ) {
        return;
    }

    for ( @{ $opts->{not_restricted} || [] } ) {
        if ( $self->page_matches( $page, $_ ) ) {
            if ( ref eq 'HASH' ) {
                if ( exists $_->{role} ) {
                    return if ref $_->{role} eq 'SCALAR';
                    return exists $user_ref->{role}{ $_->{role} } ? () : 1;
                }
                else {
                    return;
                }
            }
            else {
                return;
            }
        }
    }

    for ( @{ $opts->{restricted} || [] } ) {
        if ( $self->page_matches( $page, $_ ) ) {
            defined $user_ref
                or return 1;

            if ( ref eq 'HASH' ) {
                return 1 if ref $_->{role} eq 'SCALAR';
                return exists $user_ref->{role}{ $_->{role} } ? 1 : ();
            }
            else {
                return 0;
            }
        }
    }

    return;
}

sub page_matches {
    my ( $self, $page, $what ) = @_;

    if ( ref $what eq 'Regexp' ) {
        return $page =~ /$what/ ? 1 : ();
    }
    elsif ( ref $what eq 'HASH' ) {
        if ( ref $what->{page} eq 'Regexp' ) {
            return $page =~ /$what->{page}/ ? 1 : ();
        }
        elsif ( $what->{page} eq $page ) {
            return 1;
        }
        return 0;
    }
    elsif ( $what eq $page ) {
        return 1;
    }
    return;
}

sub is_logged_in {
    my ( $self, $query, $config) = @_;

    my $opts = $self->opts;
    my ( $cookie_l, $cookie_s ) = ( $self->cookie_l, $self->cookie_s );

lib/App/ZofCMS/Plugin/UserLogin.pm  view on Meta::CPAN

    <input type="hidden" name="zofcms_plugin_login" value="logout_user">
    <tmpl_var name='logout_button'>
</div>
</form>
END_TEMPLATE
}

sub cookie_l {
    my $self = shift;
    @_ and $self->{COOKIE_L} = shift;
    $self->{COOKIE_L};
}

sub cookie_s {
    my $self = shift;
    @_ and $self->{COOKIE_S} = shift;
    $self->{COOKIE_S};
}

1;
__END__

=encoding utf8

=head1 NAME

App::ZofCMS::Plugin::UserLogin - restrict access to pages based on user accounts

=head1 SYNOPSIS

In $your_database_of_choice that is supported by L<DBI> create a table.
You can have extra columns in it, but the first five must be named as appears
below. C<login_time> is the return of Perl's C<time()>. Password will be
C<md5_hex()>ed (with L<Digest::MD5>,
C<session_id> is C<rand() . rand() . rand()> and role depends
on what you set the roles to be:

    create TABLE users (
        login TEXT,
        password VARCHAR(32),
        login_time VARCHAR(10),
        session_id VARCHAR(55),
        role VARCHAR(20)
    );

Main config file:

    template_defaults => {
        plugins => [ { UserLogin => 10000 } ],
    },
    plug_login => {
        dsn                     => "DBI:mysql:database=test;host=localhost",
        user                    => 'test', # user,
        pass                    => 'test', # pass
        opt                     => { RaiseError => 1, AutoCommit => 0 },
        table                   => 'users',
        login_page              => '/login',
        redirect_on_restricted  => '/login',
        redirect_on_login       => '/',
        redirect_on_logout      => '/',
        not_restricted          => [ qw(/ /index) ],
        restricted              => [ qr/^/ ],
        smart_deny              => 'login_redirect_page',
        preserve_login          => 'my_site_login',
        login_button => '<input type="submit"
            class="input_submit" value="Login">',
        logout_button => '<input type="submit"
            class="input_submit" value="Logout">',
    },

In L<HTML::Template> template for C<'/login'> page:

    <tmpl_var name="plug_login_form">
    <tmpl_var name="plug_login_logout">


=head1 DESCRIPTION

The module is a plugin for L<App::ZofCMS>; it provides functionality to
restrict access to some pages based on user accounts (which support "roles")

Plugin uses HTTP cookies to set user sessions.

This documentation assumes you've read L<App::ZofCMS>,
L<App::ZofCMS::Config> and L<App::ZofCMS::Template>

=head1 NOTE ON LOGINS

Plugin makes the logins B<lowercased> when doing its processing; thus C<FooBar> login
is the same as C<foobar>.

=head1 NOTE ON REDIRECTS

There are quite a few options that redirect the user upon a certain event.
The C<exit()> will be called upon a redirect so keep that
in mind when setting plugin's priority setting.

=head1 DATABASE

Plugin needs access to the database that is supported by L<DBI> module.
You'll need to create a table the format of which is described in the
first paragraph of L<SYNOPSIS> section above. B<Note>: plugin does not
support I<creation> of user accounts. That was left for other plugins
(e.g. L<App::ZofCMS::Plugin::FormToDatabase>)
considering that you are flexible in what the entry for each user in the
database can contain.

=head1 ROLES

The "role" of a user can be used to limit access only to certain users.
In the database the user can have several roles which are to be separated
by commas (C<,>). For example:

    foo,bar,baz

The user with that role is member of role "foo", "bar" and "baz".

=head1 TEMPLATE/CONFIG FILE SETTINGS

    plug_login => {
        dsn                     => "DBI:mysql:database=test;host=localhost",
        user                    => 'test',
        pass                    => 'test',
        opt                     => { RaiseError => 1, AutoCommit => 0 },
        table                   => 'users',
        user_ref    => sub {
            my ( $user_ref, $template ) = @_;
            $template->{d}{plug_login_user} = $user_ref;
        },
        login_page              => '/login',
        redirect_on_restricted  => '/login',
        redirect_on_login       => '/',
        redirect_on_logout      => '/',
        not_restricted          => [ qw(/ /index) ],
        restricted              => [ qr/^/ ],
        smart_deny              => 'login_redirect_page',
        preserve_login          => 'my_site_login',
        login_button => '<input type="submit"
            class="input_submit" value="Login">',
        logout_button => '<input type="submit"
            class="input_submit" value="Logout">',
    },

These settings can be set via C<plug_login> first-level key in ZofCMS
template, but you probably would want to set all this in main config file via
C<plug_login> first-level key.

=head2 C<dsn>

    dsn => "DBI:mysql:database=test;host=localhost",

B<Mandatory>. The C<dsn> key will be passed to L<DBI>'s C<connect_cached()>
method, see documentation for L<DBI> and C<DBD::your_database> for the
correct syntax of this one. The example above uses MySQL database called
C<test> which is location on C<localhost>

=head2 C<user>

    user => 'test',

B<Mandatory>. Specifies the user name (login) for the database. This can
be an empty string if, for example, you are connecting using SQLite driver.

=head2 C<pass>

    pass => 'test',

B<Mandatory>. Same as C<user> except specifies the password for the database.

=head2 C<table>

    table => 'users',

B<Optional>. Specifies which table in the database stores user accounts.
For format of this table see L<SYNOPSIS> section. B<Defaults to:> C<users>

=head2 C<opt>

    opt => { RaiseError => 1, AutoCommit => 0 },

B<Optional>. Will be passed directly to C<DBI>'s C<connect_cached()> method
as "options". B<Defaults to:> C<< { RaiseError => 1, AutoCommit => 0 } >>

=head2 C<user_ref>

    user_ref => sub {
        my ( $user_ref, $template ) = @_;
        $template->{d}{plug_login_user} = $user_ref;
    },

B<Optional>. Takes a subref as an argument. When specified the subref will be called and
its C<@_> will contain the following: C<$user_ref>, C<$template_ref>, C<$query_ref>,
C<$config_obj>, where C<$user_ref> will be either C<undef> (e.g. when user is not logged on)
or will contain an arrayref with user data pulled from the SQL table, i.e. an arrayref

lib/App/ZofCMS/Plugin/UserLogin.pm  view on Meta::CPAN

    login_button => '<input type="submit"
            class="input_submit" value="Login">',

B<Optional>. Takes HTML code for the login button, though, feel free to
use it as an insertion point for any extra code you might want in your
login form. B<Defaults to:>
C<< <input type="submit" class="input_submit" value="Login"> >>

=head2 C<logout_button>

    logout_button => '<input type="submit"
        class="input_submit" value="Logout">'

B<Optional>. Takes HTML code for the logout button, though, feel free to
use it as an insertion point for any extra code you might want in your
logout form. B<Defaults to:>
C<< <input type="submit" class="input_submit" value="Logout"> >>

=head2 C<redirect_on_logout>

    redirect_on_logout => '/uri',

B<Optional>. Specifies the URI to which to redirect the user after he or
she logged out.

=head2 C<restricted>

    restricted => [
        qw(/foo /bar /baz),
        qr|^/foo/|i,
        { page => '/admin', role => 'admin' },
        { page => qr|^/customers/|, role => 'customer' },
    ],

B<Optional> but doesn't make sense to not specify this one.
B<By default> is not specified. Takes an arrayref
as a value. Elements of this arrayref can be as follows:

=head3 a string

    restricted => [ qw(/foo /bar) ],

Elements that are plain strings represent direct pages ( page is made out of
$query{dir} . $query{page} ). The example above will restrict access
only to pages C<http://foo.com/index.pl?page=foo> and
C<http://foo.com/index.pl?page=bar> for users that are not logged in.

=head3 a regex

    restricted => [ qr|^/foo/| ],

Elements that are regexes (C<qr//>) will be matched against the page. If
the page matches the given regex access will be restricted to any user
who is not logged in.

=head3 a hashref

    restricted => [
        { page => '/secret', role => \1 },
        { page => '/admin', role => 'customer' },
        { page => '/admin', role => 'not_customer' },
        { page => qr|^/customers/|, role => 'not_customer' },
    ],

Using hashrefs you can set specific roles that are restricted from a given
page. The hashref must contain two keys: the C<page> key and C<role> key.
The value of the C<page> key can be either a string or a regex which will
be matched against the current page the same way as described above. The
C<role> key must contain a role of users that B<are restricted> from
accessing the page specified by C<page> key or a scalarref
(meaning "any role"). B<Note> you can specify only
B<one> role per hashref. If you want to have several roles you need to
specify several hashrefs or use C<not_restricted> option described below.

In the example above only logged in users who are B<NOT> members of role
C<customer> or C<not_customer> can access C</admin> page and
only logged in users who are B<NOT> members of role C<not_customer>
can access pages that begin with C</customers/>. The page C</secret> is
restricted for B<everyone> (see note on scalarref below).

B<IMPORTANT NOTE:> the restrictions will be checked until the first one
matching the page criteria found. Therefore, make sure to place
the most restrictive restrictions first. In other words:

    restricted => [
        qr/^/,
        { page => '/foo', role => \1 },
    ],

Will B<NOT> block logged in users from page C</foo> because C<qr/^/> matches
first. Proper way to write this restriction would be:

    restricted => [
        { page => '/foo', role => \1 },
        qr/^/,
    ],

B<Note:> the role can also be a I<scalarref>; if it is, it means "any role".
In other words:

    restricted => [ qr/^/ ],

Means "all the pages are restricted for users who are not logged in". While:

    restricted => [ { page => qr/^/, role \1 } ],

Means that "all pages are restricted for everyone" (in this case you'd use
C<not_restricted> option described below to ease the restrictions).


=head2 C<not_restricted>

    not_restricted => [
        qw(/foo /bar /baz),
        qr|^/foo/|i,
        { page => '/garbage', role => \1 },
        { page => '/admin', role => 'admin' },
        { page => qr|^/customers/|, role => 'customer' },
    ],

B<Optional>. The value is the exact same format as for C<restricted> option
described above. B<By default> is not specified.
The purpose of C<not_restricted> is the reverse of
C<restricted> option. Note that pages that match anything in
C<not_restricted> option will not be checked against C<restricted>. In other
words you can construct rules such as this:

    restricted => [
        qr/^/,
        { page => qr|^/admin|, role => \1 },
    ],
    not_restricted => [
        qw(/ /index),
        { page => qr|^/admin|, role => 'admin' },
    ],

The example above will restrict access to every page on the site that is
not C</> or C</index> to any user who is not logged in. In addition, pages
that begin with C</admin> will be accessible only to users who are members
of role C<admin>.

=head2 C<limited_time>

    limited_time => 600,

B<Optional>. Takes integer values greater than 0. Specifies the amount
of seconds after which user's session expires. In other words, if you
set C<limited_time> to 600 and user went to the crapper for 10 minutes, then
came back, he's session would expire and he would have to log in again.
B<By default> not specified and sessions expire when the cookies do so
(which is "by the end of browser's session", let me know if you wish to
control that).

=head2 C<no_cookies>

    no_cookies => 1,

B<Optional>. When set to a false value plugin will set two cookies:
C<md5_hex()>ed user login and session ID. When set to a true value plugin
will not set any cookies and instead will put session ID into
C<plug_login_session_id> key under ZofCMS template's C<{t}> special key.
B<By default> is not specified (false).

=head1 HTML::Template TEMPLATE

There are two (or three, depending if you set C<no_cookies> to a true value)
keys created in ZofCMS template C<{t}> special key, thus are available in
your L<HTML::Template> templates:

=head2 C<plug_login_form>

    <tmpl_var name="plug_login_form">

The C<plug_login_form> key will contain the HTML code for the "login form".
You'd use C<< <tmpl_var name="plug_login_form"> >> on your "login page".
Note that login errors, i.e. "wrong login or password" will be automagically
display inside that form in a C<< <p class="error"> >>.

=head2 C<plug_login_logout>

    <tmpl_var name="plug_login_logout">

This one is again an HTML form except for the "logout" button. Drop it
anywhere you want.

=head2 C<plug_login_user>

    <tmpl_if name="plug_login_user">
        Logged in as <tmpl_var name="plug_login_user">.
    </tmpl_if>

The C<plug_login_user> will contain the login name of the currently logged in



( run in 2.709 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )