package Yote::AppRoot;
#########################################
# Base class for all Yote applications. #
#########################################
use strict;
use warnings;
no warnings 'uninitialized';
use File::Slurp;
use MIME::Base64;
use Yote;
use Yote::Account;
use Yote::Obj;
use Yote::RootObj;
use Yote::SimpleTemplate;
use Yote::Root;
use parent 'Yote::RootObj';
use vars qw($VERSION);
$VERSION = '0.089';
sub _init {
my $self = shift;
my $hn = `hostname`;
chomp $hn;
$self->set_app_name( ref( $self ) );
$self->set_host_name( $hn );
$self->set_host_url( "http://$hn" );
$self->set_validation_email_from( 'yote@' . $hn );
$self->set_validation_link_template(new Yote::SimpleTemplate( { text=>'${hosturl}/val.html?t=${t}&app=${app}' } ) );
$self->set_validation_message_template(new Yote::SimpleTemplate({text=>'Welcome to ${app}, ${handle}. Click on this link to validate your email : ${link}'}));
$self->set_validation_subject_template(new Yote::SimpleTemplate( { text => 'Validate Your Account' } ) );
$self->set_recovery_email_from( 'yote@' . $hn );
$self->set_recovery_subject_template(new Yote::SimpleTemplate( { text => 'Recover Your Account' } ) );
$self->set_recovery_link_template(new Yote::SimpleTemplate( { text => '${hosturl}/recover.html?t=${t}&app=${app}' } ) );
$self->set_recovery_message_template(new Yote::SimpleTemplate({text=>'Click on <a href="${link}">${link}</a> to recover your account' } ) );
$self->set__attached_objects( {} ); # field -> obj parings, set aside here as a duplicate data structure to track items that may be editable on the admin page
$self->SUPER::_init();
} #_init
sub _load {
my $self = shift;
$self->get__attached_objects( {} ); # field -> obj parings, set aside here as a duplicate data structure to track items that may be editable on the admin page
}
# ------------------------------------------------------------------------------------------
# * PUBLIC API Methods *
# ------------------------------------------------------------------------------------------
#
# Return the account object for this app.
#
sub account {
my( $self, $data, $account ) = @_;
return $account;
} #account
sub admin_prefetch {
my( $self, $data, $acct ) = @_;
if( $acct && $acct->is_root() ) {
return {
attached_objects => $self->get__attached_objects({}),
};
}
} #admin_prefect
sub create_login {
my( $self, $args, $dummy, $env ) = @_;
my( $handle, $email, $password ) = ( $args->{h}, $args->{e}, $args->{p} );
if( $self->get_requires_validation() && ! $email ) {
die "Must specify valid email";
}
my $root = Yote::Root::fetch_root();
my $login = $root->_create_login( $handle, $email, $password, $env );
if( $self->get_requires_validation() ) {
my $rand_token = $root->_register_login_with_validation_token( $login );
my $link = $self->get_validation_link_template()->_fill( {
t => $rand_token,
hosturl => $self->get_host_url(),
} );
my $context = {
handle => $handle,
email => $email,
app => $self->get_app_name( ref( $self ) ),
link => $link,
};
Yote::IO::Mailer::send_email(
{
to => $email,
from => $self->get_validation_email_from( 'yote@' . $self->get_host_name() ),
subject => $self->get_validation_subject_template()->_fill( $context ),
msg => $self->get_validation_message_template()->_fill( $context ),
} );
} #requires validation
return { l => $login, t => $root->_create_token( $login, $env->{REMOTE_ADDR} ) };
} #create_login
#
#
#
sub do_404 {
my $self = shift;
return ( 404, $self->get_error_page() );
} #do_404
#
# Fetches objects by id list
#
sub fetch {
my( $self, $data, $account, $env ) = @_;
die "Access Error" unless Yote::ObjManager::allows_access( $data, $self, $account ? $account->get_login() : undef, $env->{GUEST_TOKEN} );
if( ref( $data ) eq 'ARRAY' ) {
my $login = $account->get_login();
return [ map { Yote::ObjProvider::fetch( $_ ) } grep { defined($Yote::ObjProvider::LOGIN_OBJECTS->{ $login->{ID} }{ $_ }) } @$data ];
}
return [ Yote::ObjProvider::fetch( $data ) ];
} #fetch
#
# TODO: maybe rather than app_name and DEfAULT, have a html_root and html_root_default fields
# Returns response code and response.
#
sub fetch_page {
my( $self, $url ) = @_;
my $node = $self->_hash_fetch( '_pages', $url );
my $file_loc = "$ENV{YOTE_ROOT}/html/".$self->get_app_name()."/$url";
my $app_default_loc = $self->get_app_default_loc();
my $default_file_loc = defined( $app_default_loc ) ? "$ENV{YOTE_ROOT}/html/$app_default_loc" : undef;
if( -e $file_loc ) {
if( $node ) {
# check to see which is more recent. if the file is, then set the current version of the node to the file unless
# the node version is locked.
my $file_mod_time;
my $last_updated = $node->get_last_updated();
if( $file_mod_time > $last_updated ) {
my $html = read_file( $url );
if( ! $node->get_version_locked() ) {
$node->set_current_version_number( $node->_count( { name => 'versions' } ) );
$node->add_to_versions( $html );
$node->set_current_version( $html );
}
return 200, $html;
}
elsif( $file_mod_time < $last_updated ) {
my $html = $node->get_current_version();
write_file( $url, $html );
return 200, $html;
}
else {
return 200, $node->get_current_version();
}
}
else {
#create a new node
my $html = read_file( $file_loc );
$self->_hash_insert( '_pages',
$url,
$node = new Yote::RootObj( { current_version => $html,
versions => [ $html ],
last_updated => time } ) );
return $html;
}
}
elsif( $node ) {
my $html = $node->get_current_version();
write_file( $file_loc, $html );
return 200, $html;
}
elsif( $default_file_loc && -e $default_file_loc ) {
return 200, read_file( $default_file_loc );
}
else {
return 404, $self->do_404();
}
} #fetch_page
sub make_root {
my( $self, $login, $acct ) = @_;
die "Access Error" unless $acct->is_root();
$login->set__is_root( 1 );
$login->set_is_root( 1 );
return "made root";
} #make_root
sub new_obj {
my( $self, $data, $acct ) = @_;
my $ret = new Yote::Obj( ref( $data ) ? $data : undef );
$ret->set___creator( $acct );
return $ret;
} #new_obj
sub new_root_obj {
my( $self, $data, $acct ) = @_;
return "Access Error" unless $acct && $acct->get_login() && $acct->get_login()->is_root();
my $ret = new Yote::RootObj( ref( $data ) ? $data : undef );
$ret->set___creator( $acct );
return $ret;
} #new_root_obj
sub new_template {
my( $self, $data, $acct ) = @_;
return "Access Error" unless $acct && $acct->get_login() && $acct->get_login()->is_root();
my $ret = new Yote::SimpleTemplate();
$ret->set___creator( $acct );
return $ret;
} #new_template
sub new_user_obj {
my( $self, $data, $acct ) = @_;
my $ret = new Yote::UserObj( ref( $data ) ? $data : undef );
$ret->set___creator( $acct );
return $ret;
} #new_user_obj
#
# Sends an email to the address containing a link to reset password.
#
sub recover_password {
my( $self, $email ) = @_;
my $root = Yote::Root::fetch_root();
my $login = $root->_hash_fetch( '_emails', $email );
if( $login ) {
my $now = time();
unless( $login || ( $now - $login->get__last_recovery_time() ) < (60*15) ) { #need to wait 15 mins
die "password recovery attempt failed";
}
else {
my $rand_token = int( rand 9 x 10 );
my $recovery_hash = $root->get__recovery_logins({});
my $times = 0;
while( $recovery_hash->{$rand_token} && ++$times < 100 ) {
$rand_token = int( rand 9 x 10 );
}
if( $recovery_hash->{$rand_token} ) {
die "error recovering password";
}
$login->set__recovery_token( $rand_token );
$login->set__last_recovery_time( $now );
$login->set__recovery_tries( $login->get__recovery_tries() + 1 );
$recovery_hash->{$rand_token} = $login;
my $link = $self->get_recovery_link_template()->_fill(
{
t => $rand_token,
hosturl => $self->get_host_url(),
app => ref( $self ),
} );
my $context = {
handle => $login->get_handle(),
email => $email,
app => $self->get_app_name(),
link => $link,
app => ref( $self ),
};
Yote::IO::Mailer::send_email(
{
to => $email,
from => $self->get_recovery_email_from(),
subject => $self->get_recovery_subject_template()->_fill( $context ),
msg => $self->get_recovery_message_template()->_fill( $context ),
} );
}
} #if login
return "password recovery initiated";
} #recover_password
#
# reset by a recovery link.
#
sub recovery_reset_password {
my( $self, $args ) = @_;
my $root = Yote::Root::fetch_root();
my $newpass = $args->{p};
my $rand_token = $args->{t};
my $recovery_hash = $root->get__recovery_logins({});
my $login = $recovery_hash->{$rand_token};
if( $login ) {
my $now = $login->get__last_recovery_time();
delete $recovery_hash->{$rand_token};
if( ( time() - $now ) < 3600 * 24 ) { #expires after a day
$login->set__password( Yote::encrypt_pass( $newpass, $login->get_handle() ) );
$login->set__is_validated(1);
return $login->get__recovery_from_url();
}
}
die "Recovery Link Expired or not valid";
} #recovery_reset_password
sub remove_account {
my( $self, $args, $acct, $env ) = @_;
die "invalid arguments" unless ref( $args ) eq 'HASH';
my( $del_acct, $password ) = @$args['a','p'];
die "invalid arguments" unless $del_acct && $password;
die "Cannot remove root" if $del_acct->get_login()->is_root() || $del_acct->get_login()->is_master_root();
my $login = $del_acct->get_login();
if( $acct->is_root() ||
( $del_acct->_is( $acct ) &&
Yote::encrypt_pass($password, $login->get_handle()) eq $login->get__password() ) ) {
$self->_hash_delete( '_account_roots', $login->{ID} );
}
die "unable to remove account";
} #remove_account
#
# Used by the web app server to verify the login. Returns the login object belonging to the token.
#
sub token_login {
my( $self, $t, undef, $env ) = @_;
my $ip = $env->{ REMOTE_ADDR };
if( $t =~ /(.+)\-(.+)/ ) {
my( $uid, $token ) = ( $1, $2 );
my $login = $self->_fetch( $uid );
if( ref( $login ) && ref( $login ) ne 'HASH' && ref( $login ) ne 'ARRAY'
&& $login->get__token() eq "${token}x$ip" ) {
return $login;
}
}
return;
} #token_login
sub validate {
my( $self, $token ) = @_;
my $root = Yote::Root::fetch_root();
return $root->_validate( $token );
} #validate
# ------------------------------------------------------------------------------------------
# * PRIVATE METHODS *
# ------------------------------------------------------------------------------------------
###################################
# These methods may be overridden #
###################################
#
# Intializes the account object passed in.
#
sub _init_account {}
#
# Override to use different classes for the account objects.
#
sub _new_account {
return new Yote::Account();
}
#######################################################
# Fixed ( should not be overridden ) utility methods. #
#######################################################
#
# Returns the account root attached to this AppRoot for the given account. Not meant to be overridden.
#
sub __get_account {
my( $self, $login ) = @_;
my $acct = $self->_hash_fetch( '_account_roots', $login->{ID} );
unless( $acct ) {
$acct = $self->_new_account();
$acct->set_login( $login );
$acct->set_handle( $login->get_handle() );
$self->_hash_insert( '_account_roots', $login->{ID}, $acct );
$self->_init_account( $acct );
}
die "Access Error" if $acct->get__is_disabled();
return $acct;
} #__get_account
1;
__END__
=head1 NAME
Yote::AppRoot - Application Server Base Objects
=head1 DESCRIPTION
This is the root class for all Yote Apps. Extend it to create an App Object.
Each Web Application has a single container object as the entry point to that object which is an instance of the Yote::AppRoot class.
A Yote::AppRoot extends Yote::Obj and provides some class methods and the following stub methods.
=head1 PUBLIC API METHODS
=over 4
=item account
Return the account that the user has with this app.
=item do_404
=item check_guest_token
=item create_login( args )
Create a login with the given client supplied args : h => handle, e => email, p => password.
This checks to make sure handle and email address are not already taken.
This is invoked by the javascript call $.yote.create_login( handle, password, email )
=item fetch
=item fetch_page
=item make_root
=item new_obj
=item new_root_obj
=item new_template
=item new_user_obj
=item precache
Meant to be overridden. Returns all data to the client or html page that the app in order to not need lazy loading.
=item recover_password( { e : email, u : a_url_the_person_requested_recovery, t : reset_url_for_system } )
Causes an email with a recovery link sent to the email in question, if it is associated with an account.
Returns the currently logged in account using this app.
=item recovery_reset_password( { p : newpassword, p2 : newpasswordverify, t : recovery_token } )
Resets the password ( kepts hashed in the database ) for the account that the recovery token belongs to.
Returns the url_the_person_requested_recovery that was given in the recover_password call.
=item remove_account( { h : handle, e : email, p : password } )
=item remove_login( { h : handle, e : email, p : password } )
Purges the login account from the system if its credentials are verified. It moves the account to a special removed logins hidden field under the yote root.
=item token_login()
Returns a token that is used by the client and server to sync up data for the case of a user not being logged in.
=item validate( rand_token )
Validates and returns the login specified by the random token.
=back
=head1 OVERRIDABLE METHODS
=over 4
=item _init_account( $acct )
This is called whenever a new account is created for this app. This can be overridden to perform any initialzation on the
account.
=item _new_account()
This returns a new Yote::Account object to be used with this app. May be overridden to return a subclass of Yote::Account.
=back
=head1 PUBLIC DATA FIELDS
=over 4
=item requires_validation
When true, an account will not work until validation of the login is achieved, through email or other means.
=back
=head1 PRIVATE DATA FIELDS
=over 4
=item _account_roots
This is a hash of login ID to account.
=back
=head1 AUTHOR
Eric Wolf
coyocanid@gmail.com
http://madyote.com
=head1 LICENSE AND COPYRIGHT
Copyright (C) 2011 Eric Wolf
This module is free software; it can be used under the same terms as perl
itself.
=cut