use warnings;
use strict;

package Jifty::API;

=head1 NAME

Jifty::API - Manages and allow reflection on the Jifty::Actions that
make up a Jifty application's API

=head1 SYNOPSIS

 # Find the full name of an action
 my $class = Jifty->api->qualify('SomeAction');

 # Logged users with an ID greater than 10 have restrictions
 if (Jifty->web->current_user->id > 10) {
     Jifty->api->deny('Foo');
     Jifty->api->allow('FooBar');
     Jifty->api->deny('FooBarDeleteTheWorld');
 }

 # Fetch the class names of all the allowed actions
 my @actions = Jifty->api->actions;

 # Check to see if an action is allowed
 if (Jifty->api->is_allowed('TrueFooBar')) {
     # do something...
 }

 # Undo all allow/deny/restrict calls
 Jifty->api->reset;

=head1 DESCRIPTION

You can fetch an instance of this class by calling L<Jifty/api> in your application. This object can be used to examine the actions available within your application and manage access to those actions.

=cut


use base qw/Class::Accessor::Fast Jifty::Object/;


__PACKAGE__->mk_accessors(qw(action_limits));

=head1 METHODS

=head2 new

Creates a new C<Jifty::API> object.

Don't use this, see L<Jifty/api> to access a reference to C<Jifty::API> in your application.

=cut

sub new {
    my $class = shift;
    my $self  = bless {}, $class;

    # Setup the basic allow/deny rules
    $self->reset;

    # Find all the actions for the API reference (available at _actions)
    Jifty::Module::Pluggable->import(
        search_path => [
            Jifty->app_class("Action"),
            "Jifty::Action",
            map {ref($_)."::Action"} Jifty->plugins,
        ],
        except   => qr/\.#/,
        sub_name => "_actions",
    );

    return ($self);
}

=head2 qualify ACTIONNAME

Returns the fully qualified package name for the given provided
action.  If the C<ACTIONNAME> starts with C<Jifty::> or
C<ApplicationClass::Action>, simply returns the given name; otherwise,
it prefixes it with the C<ApplicationClass::Action>.

=cut

sub qualify {
    my $self   = shift;
    my $action = shift;

    # Get the application class name
    my $base_path = Jifty->app_class;

    # Return the class now if it's already fully qualified
    return $action
        if ($action =~ /^Jifty::/
        or $action =~ /^\Q$base_path\E::/);

    # Otherwise qualify it
    return $base_path . "::Action::" . $action;
}

=head2 reset

Resets which actions are allowed to the defaults; that is, all of the
application's actions, L<Jifty::Action::Autocomplete>, and
L<Jifty::Action::Redirect> are allowed; everything else is denied.
See L</restrict> for the details of how limits are processed.

=cut

sub reset {
    my $self = shift;

    # Set up defaults
    my $app_actions = Jifty->app_class("Action");

    # These are the default action limits
    $self->action_limits(
        [   { deny => 1, restriction => qr/.*/ },
            {   allow       => 1,
                restriction => qr/^\Q$app_actions\E/,
            },
            { allow => 1, restriction => 'Jifty::Action::Autocomplete' },
            { allow => 1, restriction => 'Jifty::Action::Redirect' },
        ]
    );
}

=head2 allow RESTRICTIONS

Takes a list of strings or regular expressions, and adds them in order
to the list of limits for the purposes of L</is_allowed>.  See
L</restrict> for the details of how limits are processed.

=cut

sub allow {
    my $self = shift;
    $self->restrict( allow => @_ );
}

=head2 deny RESTRICTIONS

Takes a list of strings or regular expressions, and adds them in order
to the list of limits for the purposes of L</is_allowed>.  See
L</restrict> for the details of how limits are processed.

=cut

sub deny {
    my $self = shift;
    $self->restrict( deny => @_ );
}

=head2 restrict POLARITY RESTRICTIONS

Method that L</allow> and L</deny> call internally; I<POLARITY> is
either C<allow> or C<deny>.  Allow and deny limits are evaluated in
the order they're called.  The last limit that applies will be the one
which takes effect.  Regexes are matched against the class; strings
are fully L</qualify|qualified> and used as an exact match against the
class name.  The base set of restrictions (which is reset every
request) is set in L</reset>, and usually modified by the
application's L<Jifty::Dispatcher> if need be.

If you call:

    Jifty->api->deny  ( qr'Foo' );
    Jifty->api->allow ( qr'FooBar' );
    Jifty->api->deny  ( qr'FooBarDeleteTheWorld' );

..then:

    calls to MyApp::Action::Baz will succeed.
    calls to MyApp::Action::Foo will fail.
    calls to MyApp::Action::FooBar will pass.
    calls to MyApp::Action::TrueFoo will fail.
    calls to MyApp::Action::TrueFooBar will pass.
    calls to MyApp::Action::TrueFooBarDeleteTheWorld will fail.
    calls to MyApp::Action::FooBarDeleteTheWorld will fail.

=cut

sub restrict {
    my $self         = shift;
    my $polarity     = shift;
    my @restrictions = @_;

    # Check the sanity of the polarity
    die "Polarity must be 'allow' or 'deny'"
        unless $polarity eq "allow"
        or $polarity     eq "deny";

    for my $restriction (@restrictions) {

        # Don't let the user "allow .*"
        die "For security reasons, Jifty won't let you allow all actions"
            if $polarity eq "allow"
            and ref $restriction
            and $restriction =~ /^\(\?[-xism]*:\^?\.\*\$?\)$/;

        # Fully qualify it if it's a string
        $restriction = $self->qualify($restriction)
            unless ref $restriction;

        # Add to list of restrictions
        push @{ $self->action_limits },
            { $polarity => 1, restriction => $restriction };
    }
}

=head2 is_allowed CLASS

Returns true if the I<CLASS> name (which is fully qualified if it is
not already) is allowed to be executed.  See L</restrict> above for
the rules that the class name must pass.

=cut

sub is_allowed {
    my $self  = shift;
    my $class = shift;

    # Qualify the action
    $class = $self->qualify($class);

    # Assume that it doesn't pass; however, the real fallbacks are
    # controlled by L</reset>, above.
    my $allow = 0;

    # Walk all of the limits
    for my $limit ( @{ $self->action_limits } ) {

        # Regexes are =~ matches, strigns are eq matches
        if ( ( ref $limit->{restriction} and $class =~ $limit->{restriction} )
            or ( $class eq $limit->{restriction} ) )
        {

            # If the restriction passes, set the current allow/deny
            # bit according to if this was a positive or negative
            # limit
            $allow = $limit->{allow} ? 1 : 0;
        }
    }
    return $allow;
}

=head2 actions

Lists the class names of all of the allowed actions for this Jifty
application; this may include actions under the C<Jifty::Action::>
namespace, in addition to your application's actions.

=cut

sub actions {
    my $self = shift;
    return sort grep { $self->is_allowed($_) } $self->_actions;
}

=head1 SEE ALSO

L<Jifty>, L<Jifty::Web>, L<Jifty::Action>

=head1 LICENSE

Jifty is Copyright 2005-2006 Best Practical Solutions, LLC. 
Jifty is distributed under the same terms as Perl itself.

=cut

1;