The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Silki::Schema::User;
{
  $Silki::Schema::User::VERSION = '0.29';
}

use strict;
use warnings;
use namespace::autoclean;

use feature ':5.10';

use Authen::Passphrase::BlowfishCrypt;
use DateTime;
use Digest::SHA qw( sha1_hex );
use Fey::Literal::Function;
use Fey::Object::Iterator::FromSelect;
use Fey::ORM::Exceptions qw( no_such_row );
use Fey::Placeholder;
use List::AllUtils qw( all any none first first_index );
use Moose::Util::TypeConstraints;
use Silki::Email qw( send_email );
use Silki::I18N qw( loc );
use Silki::Schema;
use Silki::Schema::Domain;
use Silki::Schema::Page;
use Silki::Schema::Permission;
use Silki::Schema::Role;
use Silki::Types qw( Int Str Bool );
use Silki::Util qw( string_is_empty );

use Fey::ORM::Table;
use MooseX::ClassAttribute;
use MooseX::Params::Validate qw( pos_validated_list validated_list );

my $Schema = Silki::Schema->Schema();

with 'Silki::Role::Schema::URIMaker';

with 'Silki::Role::Schema::SystemLogger' =>
    { methods => [ 'insert', 'update', 'delete' ] };

with 'Silki::Role::Schema::DataValidator' => {
    steps => [
        '_has_password_or_openid_uri',
        '_email_address_is_unique',
        '_normalize_and_validate_openid_uri',
        '_openid_uri_is_unique',
    ],
};

has_policy 'Silki::Schema::Policy';

has_table( $Schema->table('User') );

has_one 'creator' => ( table => $Schema->table('User') );

has_one 'image' => (
    table => $Schema->table('UserImage'),
    undef => 1,
);

has_many 'pages' => ( table => $Schema->table('Page') );

has best_name => (
    is      => 'ro',
    isa     => Str,
    lazy    => 1,
    builder => '_build_best_name',
    clearer => '_clear_best_name',
);

has has_valid_password => (
    is      => 'ro',
    isa     => Bool,
    lazy    => 1,
    builder => '_build_has_valid_password',
    clearer => '_clear_has_valid_password',
);

has has_login_credentials => (
    is      => 'ro',
    isa     => Bool,
    lazy    => 1,
    builder => '_build_has_login_credentials',
    clearer => '_clear_has_login_credentials',
);

class_has _RoleInWikiSelect => (
    is      => 'ro',
    does    => 'Fey::Role::SQL::ReturnsData',
    lazy    => 1,
    builder => '_BuildRoleInWikiSelect',
);

class_has _MemberWikiCountSelect => (
    is      => 'ro',
    does    => 'Fey::Role::SQL::ReturnsData',
    lazy    => 1,
    builder => '_BuildMemberWikiCountSelect',
);

class_has _MemberWikiSelect => (
    is      => 'ro',
    does    => 'Fey::Role::SQL::ReturnsData',
    lazy    => 1,
    builder => '_BuildMemberWikiSelect',
);

class_has _AllWikiCountSelect => (
    is      => 'ro',
    does    => 'Fey::Role::SQL::ReturnsData',
    lazy    => 1,
    builder => '_BuildAllWikiCountSelect',
);

class_has _AllWikiSelect => (
    is      => 'ro',
    does    => 'Fey::Role::SQL::ReturnsData',
    lazy    => 1,
    builder => '_BuildAllWikiSelect',
);

class_has _SharedWikiSelect => (
    is      => 'ro',
    isa     => 'Fey::SQL::Union',
    lazy    => 1,
    builder => '_BuildSharedWikiSelect',
);

class_has _RecentlyViewedPagesSelect => (
    is      => 'ro',
    isa     => 'Fey::SQL::Select',
    lazy    => 1,
    builder => '_BuildRecentlyViewedPagesSelect',
);

class_has _AllPageCountSelect => (
    is      => 'ro',
    does    => 'Fey::Role::SQL::ReturnsData',
    lazy    => 1,
    builder => '_BuildAllPageCountSelect',
);

class_has _AllRevisionCountSelect => (
    is      => 'ro',
    does    => 'Fey::Role::SQL::ReturnsData',
    lazy    => 1,
    builder => '_BuildAllRevisionCountSelect',
);

class_has _AllFileCountSelect => (
    is      => 'ro',
    does    => 'Fey::Role::SQL::ReturnsData',
    lazy    => 1,
    builder => '_BuildAllFileCountSelect',
);

class_has 'SystemUser' => (
    is      => 'ro',
    isa     => __PACKAGE__,
    lazy    => 1,
    default => sub { __PACKAGE__->_FindOrCreateSystemUser() },
);

class_has 'GuestUser' => (
    is      => 'ro',
    isa     => __PACKAGE__,
    lazy    => 1,
    default => sub { __PACKAGE__->_FindOrCreateGuestUser() },
);

class_has _ActiveUserCountSelect => (
    is      => 'ro',
    isa     => 'Fey::SQL::Select',
    lazy    => 1,
    builder => '_BuildActiveUserCountSelect',
);

class_has _ActiveUsersSelect => (
    is      => 'ro',
    isa     => 'Fey::SQL::Select',
    lazy    => 1,
    builder => '_BuildActiveUsersSelect',
);

class_has _AllUsersSelect => (
    is      => 'ro',
    isa     => 'Fey::SQL::Select',
    lazy    => 1,
    builder => '_BuildAllUsersSelect',
);

class_has _AllRevisionsForDeleteSelect => (
    is      => 'ro',
    isa     => 'Fey::SQL::Select',
    lazy    => 1,
    builder => '_BuildAllRevisionsForDeleteSelect',
);

{
    my $select = __PACKAGE__->_MemberWikiCountSelect();

    query member_wiki_count => (
        select      => $select,
        bind_params => sub { $_[0]->user_id(), $select->bind_params() },
    );
}

{
    my $select = __PACKAGE__->_MemberWikiSelect();

    has_many member_wikis => (
        table       => $Schema->table('Wiki'),
        select      => $select,
        bind_params => sub { $_[0]->user_id(), $select->bind_params() },
    );
}

{
    my $select = __PACKAGE__->_AllWikiCountSelect();

    query all_wiki_count => (
        select      => $select,
        bind_params => sub { ( $_[0]->user_id() ) x 3 },
    );
}

{
    my $select = __PACKAGE__->_AllWikiSelect();

    has_many all_wikis => (
        table       => $Schema->table('Wiki'),
        select      => $select,
        bind_params => sub { ( $_[0]->user_id() ) x 3 },
    );
}

{
    my $select = __PACKAGE__->_AllPageCountSelect();

    query page_count => (
        select      => $select,
        bind_params => sub { ( $_[0]->user_id() ) },
    );
}

{
    my $select = __PACKAGE__->_AllRevisionCountSelect();

    query revision_count => (
        select      => $select,
        bind_params => sub { ( $_[0]->user_id() ) },
    );
}

{
    my $select = __PACKAGE__->_AllFileCountSelect();

    query file_count => (
        select      => $select,
        bind_params => sub { ( $_[0]->user_id() ) },
    );
}

with 'Silki::Role::Schema::Serializes' => {
    skip => ['password'],
};

my $UnusablePW = '*unusable*';
around insert => sub {
    my $orig  = shift;
    my $class = shift;
    my %p     = @_;

    if ( $p{requires_activation} && $p{disable_login} ) {
        die loc(
            'Cannot pass requires_activation and disable_login when inserting a user'
        );
    }

    if ( delete $p{requires_activation} ) {
        $p{confirmation_key}
            = $class->_make_confirmation_key( $p{email_address} );

        $p{password} = $UnusablePW;
    }
    elsif ( delete $p{disable_login} ) {
        $p{password}         = $UnusablePW;
        $p{openid_uri}       = undef;
        $p{confirmation_key} = undef;
    }
    elsif ( defined $p{password} ) {
        $p{password} = $class->_password_as_rfc2307( $p{password} );
    }

    $p{username} //= $p{email_address};

    $p{created_by_user_id} = $p{user}->user_id()
        if $p{user};

    return $class->$orig(%p);
};

around update => sub {
    my $orig = shift;
    my $self = shift;
    my %p    = @_;

    if (  !string_is_empty( $p{email_address} )
        && string_is_empty( $p{username} )
        && $self->username() eq $self->email_address() ) {
        $p{username} = $p{email_address};
    }

    unless ( string_is_empty( $p{password} ) ) {
        $p{password} = $self->_password_as_rfc2307( $p{password} );
    }

    if ( delete $p{disable_login} ) {
        $p{password}         = $UnusablePW;
        $p{openid_uri}       = undef;
        $p{confirmation_key} = undef;
    }

    $p{last_modified_datetime} = Fey::Literal::Function->new('NOW');

    return $self->$orig(%p);
};

after update => sub {
    $_[0]->_clear_best_name();
    $_[0]->_clear_has_valid_password();
    $_[0]->_clear_has_login_credentials();
};

around delete => sub {
    my $orig = shift;
    my $self = shift;
    my %p    = @_;

    Silki::Schema->RunInTransaction(
        sub {
            my $revisions = $self->_all_revisions_for_delete();
            while ( my $revision = $revisions->next() ) {
                $revision->delete( user => $p{user} );
            }

            $self->$orig(%p);
        }
    );
};

sub _system_log_values_for_insert {
    my $class = shift;
    my %p     = @_;

    my $msg = 'Created user: ' . $p{username};

    return (
        message   => $msg,
        data_blob => \%p,
    );
}

sub _system_log_values_for_update {
    my $self = shift;
    my %p    = @_;

    my $msg;
    if (   exists $p{is_disabled}
        && $p{is_disabled}
        && !$self->is_disabled() ) {

        $msg = 'Disabled user';
    }
    elsif ($self->is_disabled()
        && exists $p{is_disabled}
        && !$p{is_disabled} ) {

        $msg = 'Enabled user';
    }
    else {
        $msg = 'Updated user';
    }

    $msg .= ': ' . $self->best_name();

    my %blob = %p;
    delete $blob{last_modified_datetime};

    return (
        message   => $msg,
        data_blob => \%blob,
    );
}

sub _system_log_values_for_delete {
    my $self = shift;

    my $msg
        = 'Deleted user '
        . $self->best_name() . ' - '
        . ( $self->email_address() || $self->openid_uri() );

    return (
        message   => $msg,
        data_blob => {
            map { $_ => $self->$_() }
                qw(
                email_address
                username
                openid_uri
                display_name
                time_zone
                locale_code
                )
        }
    );
}

sub _make_confirmation_key {
    shift;

    return sha1_hex(
        shift, time, $$, rand(1_000_000_000),
        Silki::Config->instance()->secret()
    );
}

sub _password_as_rfc2307 {
    my $self = shift;
    my $pw   = shift;

    # XXX - require a certain length or complexity? make it
    # configurable?
    my $pass = Authen::Passphrase::BlowfishCrypt->new(
        cost        => 8,
        salt_random => 1,
        passphrase  => $pw,
    );

    return $pass->as_rfc2307();
}

sub _has_password_or_openid_uri {
    my $self      = shift;
    my $p         = shift;
    my $is_insert = shift;

    my $error = { message => loc('You must provide a password or OpenID.') };

    if ($is_insert) {

        # coverage - this is never going to be true if the around modifier on
        # insert defined above runs first, because it deletes the
        # disable_login parameter and sets a crypted password in the params
        # hash.
        return if $p->{disable_login};

        return $error
            if all { string_is_empty( $p->{$_} ) } qw( password openid_uri );

        return;
    }
    else {
        my $preserve_pw = delete $p->{preserve_password};

        # The preserve_password param will be set when a user is updated via a
        # preferences form of some sort. If they already have a valid
        # password, an empty password field in that form does not indicate
        # that the password in the dbms should be set to NULL.
        if (   exists $p->{password}
            && string_is_empty( $p->{password} )
            && $self->has_valid_password()
            && $preserve_pw ) {
            delete $p->{password};

            return;
        }

        return $error
            if all { exists $p->{$_} && string_is_empty( $p->{$_} ) }
            qw( password openid_uri );

        return
            if any { !string_is_empty( $p->{$_} ) } qw( password openid_uri );

        return if none { exists $p->{$_} } qw( password openid_uri );

        if ( exists $p->{password} && string_is_empty( $p->{password} ) ) {
            return unless string_is_empty( $self->openid_uri() );
        }
        elsif ( exists $p->{openid_uri}
            && string_is_empty( $p->{openid_uri} ) ) {
            return unless string_is_empty( $self->password() );
        }

        return $error;
    }
}

sub _email_address_is_unique {
    my $self      = shift;
    my $p         = shift;
    my $is_insert = shift;

    return if string_is_empty( $p->{email_address} );

    return if !$is_insert && $self->email_address() eq $p->{email_address};

    return unless __PACKAGE__->new( email_address => $p->{email_address} );

    return {
        field   => 'email_address',
        message => loc(
            'The email address you provided is already in use by another user.'
        ),
    };
}

sub _normalize_and_validate_openid_uri {
    my $self      = shift;
    my $p         = shift;
    my $is_insert = shift;

    return if string_is_empty( $p->{openid_uri} );

    my $uri = URI->new( $p->{openid_uri} );

    unless ( defined $uri->scheme()
        && $uri->scheme() =~ /^https?/ ) {
        return {
            field   => 'openid_uri',
            message => loc('The OpenID you provided is not a valid URI.'),
        };
    }

    $p->{openid_uri} = $uri->canonical() . q{};

    return;
}

sub _openid_uri_is_unique {
    my $self      = shift;
    my $p         = shift;
    my $is_insert = shift;

    return if string_is_empty( $p->{openid_uri} );

    return
        if !$is_insert
            && $self->openid_uri()
            && $self->openid_uri() eq $p->{openid_uri};

    return unless __PACKAGE__->new( openid_uri => $p->{openid_uri} );

    return {
        field   => 'openid_uri',
        message => loc(
            'The OpenID URI you provided is already in use by another user.'
        ),
    };
}

sub _base_uri_path {
    my $self = shift;

    return '/user/' . $self->user_id();
}

sub domain {
    my $self = shift;

    my $wiki = $self->member_wikis()->next();
    $wiki ||= $self->all_wikis()->next();

    return $wiki ? $wiki->domain() : Silki::Schema::Domain->DefaultDomain();
}

sub EnsureRequiredUsersExist {
    my $class = shift;

    $class->_FindOrCreateSystemUser();

    $class->_FindOrCreateGuestUser();
}

{
    my $SystemUsername = 'system-user';

    sub _FindOrCreateSystemUser {
        my $class = shift;

        return $class->_FindOrCreateSpecialUser($SystemUsername);
    }
}

{
    my $GuestUsername = 'guest-user';

    sub _FindOrCreateGuestUser {
        my $class = shift;

        return $class->_FindOrCreateSpecialUser(
            $GuestUsername,
            Silki::Schema::User->SystemUser(),
        );
    }

    sub is_guest {
        my $self = shift;

        return $self->username() eq $GuestUsername;
    }
}

sub _FindOrCreateSpecialUser {
    my $class    = shift;
    my $username = shift;
    my $creator  = shift;

    my $user = eval { $class->new( username => $username ) };

    return $user if $user;

    return $class->_CreateSpecialUser( $username, $creator );
}

sub _CreateSpecialUser {
    my $class    = shift;
    my $username = shift;
    my $creator  = shift;

    my $domain = Silki::Schema::Domain->DefaultDomain();

    my $email = 'silki-' . $username . q{@} . $domain->email_hostname();

    my $display_name = join ' ', map {ucfirst} split /-/, $username;

    local $Silki::Role::Schema::SystemLogger::SkipLog = 1
        unless $creator;

    return $class->insert(
        display_name   => $display_name,
        username       => $username,
        email_address  => $email,
        password       => q{},
        disable_login  => 1,
        is_system_user => 1,
        user           => $creator,
    );
}

sub set_time_zone_for_dt {
    my $self = shift;
    my $dt   = shift;

    return $dt->clone()->set_time_zone( $self->time_zone() );
}

sub _build_best_name {
    my $self = shift;

    return $self->display_name() if length $self->display_name;

    my $username = $self->username();

    if ( $username =~ /\@/ ) {
        $username =~ s/\@.+$//;
    }

    return $username;
}

sub _build_has_valid_password {
    my $self = shift;

    return 0 if string_is_empty( $self->password() );
    return 0 unless $self->_password_is_encrypted();

    return 1;
}

sub _build_has_login_credentials {
    my $self = shift;

    return 1 if !string_is_empty( $self->openid_uri() );

    return 1 if $self->has_valid_password();

    return 0;
}

sub _password_is_encrypted {
    my $self = shift;

    return $self->password() =~ /^{CRYPT}/ ? 1 : 0;
}

sub requires_activation {
    my $self = shift;

    return defined $self->confirmation_key() && !$self->has_valid_password();
}

sub confirmation_uri {
    my $self = shift;
    my %p    = @_;

    die loc(
        'Cannot make a confirmation uri for a user who does not have a confirmation key.'
    ) unless defined $self->confirmation_key();

    my $view = $p{view} || 'preferences_form';

    $p{view} = 'confirmation/' . $self->confirmation_key() . q{/} . $view;

    return $self->uri(%p);
}

sub check_password {
    my $self = shift;
    my $pw   = shift;

    return if $self->is_system_user();

    return unless $self->has_valid_password();

    my $pass = Authen::Passphrase::BlowfishCrypt->from_rfc2307(
        $self->password() );

    return $pass->match($pw);
}

sub is_authenticated {
    my $self = shift;

    return !$self->is_system_user();
}

sub can_edit_user {
    my $self = shift;
    my $user = shift;

    return 0 if $user->is_system_user();

    return 1 if $self->is_admin();

    return 1 if $self->user_id() == $user->user_id();

    return 0;
}

sub has_permission_in_wiki {
    my $self = shift;

    return 1 if $self->is_admin();

    my ( $wiki, $perm ) = validated_list(
        \@_,
        wiki       => { isa => 'Silki::Schema::Wiki' },
        permission => { isa => 'Silki::Schema::Permission' },
    );

    my $perms = $wiki->permissions();

    my $role = $self->role_in_wiki($wiki);

    return $perms->{ $role->name() }{ $perm->name() };
}

sub is_wiki_member {
    my $self = shift;
    my ($wiki) = pos_validated_list( \@_, { isa => 'Silki::Schema::Wiki' } );

    my $role_name = $self->_role_name_in_wiki($wiki);

    return defined $role_name;
}

sub role_in_wiki {
    my $self = shift;
    my ($wiki) = pos_validated_list( \@_, { isa => 'Silki::Schema::Wiki' } );

    return Silki::Schema::Role->Guest() if $self->is_guest();

    my $role_name = $self->_role_name_in_wiki($wiki);

    $role_name ||= 'Authenticated';

    return Silki::Schema::Role->$role_name();
}

sub _role_name_in_wiki {
    my $self = shift;
    my $wiki = shift;

    my $select = $self->_RoleInWikiSelect();

    my $dbh = Silki::Schema->DBIManager()->source_for_sql($select)->dbh();

    my $row = $dbh->selectrow_arrayref(
        $select->sql($dbh),
        {},
        $wiki->wiki_id(),
        $self->user_id(),
    );

    return unless $row;

    return $row->[0];
}

sub _BuildRoleInWikiSelect {
    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    #<<<
    $select
        ->select( $Schema->table('Role')->column('name') )
        ->from( $Schema->table('Role'), $Schema->table('UserWikiRole') )
        ->where( $Schema->table('UserWikiRole')->column('wiki_id'),
                 '=', Fey::Placeholder->new() )
        ->and( $Schema->table('UserWikiRole')->column('user_id'),
               '=', Fey::Placeholder->new() );
    #>>>
    return $select;
}

sub _BuildMemberWikiCountSelect {
    my $class = shift;

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    my $distinct = Fey::Literal::Term->new(
        'DISTINCT ',
        $Schema->table('Wiki')->column('wiki_id')
    );
    my $count = Fey::Literal::Function->new( 'COUNT', $distinct );

    $select->select($count);
    $class->_MemberWikiSelectBase($select);

    return $select;
}

sub _BuildMemberWikiSelect {
    my $class = shift;

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    $select->select( $Schema->table('Wiki') );
    $class->_MemberWikiSelectBase($select);
    $select->order_by( $Schema->table('Wiki')->column('title') );

    return $select;
}

sub _MemberWikiSelectBase {
    my $class  = shift;
    my $select = shift;

    my $guest  = Silki::Schema::Role->Guest();
    my $authed = Silki::Schema::Role->Authenticated();
    my $read   = Silki::Schema::Permission->Read();

    #<<<
    $select
        ->from( $Schema->table('Wiki'), $Schema->table('UserWikiRole') )
        ->where( $Schema->table('UserWikiRole')->column('user_id'),
                 '=', Fey::Placeholder->new() );
    #>>>
    return;
}

sub wikis_shared_with {
    my $self = shift;
    my $user = shift;

    my $select = $self->_SharedWikiSelect();

    return Fey::Object::Iterator::FromSelect->new(
        classes => ['Silki::Schema::Wiki'],
        select  => $select,
        dbh => Silki::Schema->DBIManager()->source_for_sql($select)->dbh(),
        bind_params => [
            $self->user_id(), $user->user_id(),
            $self->user_id(), $self->user_id(),
            $user->user_id(), $user->user_id(),
        ],
    );
}

sub _BuildSharedWikiSelect {
    my $class = shift;

    my $explicit_wiki_select = Silki::Schema->SQLFactoryClass()->new_select();

    $explicit_wiki_select->select( $Schema->table('Wiki') );
    $class->_ExplicitWikiSelectBase($explicit_wiki_select);

    my $implicit_wiki_select = Silki::Schema->SQLFactoryClass()->new_select();

    $implicit_wiki_select->select( $Schema->table('Wiki') );
    $class->_ImplicitWikiSelectBase($implicit_wiki_select);

    my $intersect1 = Silki::Schema->SQLFactoryClass()->new_intersect;
    $intersect1->intersect( $explicit_wiki_select, $explicit_wiki_select );

    my $intersect2 = Silki::Schema->SQLFactoryClass()->new_intersect;
    $intersect2->intersect( $implicit_wiki_select, $implicit_wiki_select );

    my $union = Silki::Schema->SQLFactoryClass()->new_union;

    # To use an ORDER BY with a UNION in Pg, you specify the column as a
    # number (ORDER BY 5).
    my $title_idx = first_index { $_->name() eq 'title' }
    $Schema->table('Wiki')->columns();
    #<<<
    $union
        ->union( $intersect1, $intersect2 )
        ->order_by( Fey::Literal::Term->new($title_idx) );
    #>>>
    return $union;
}

sub _BuildAllWikiCountSelect {
    my $class = shift;

    my $explicit_wiki_select = Silki::Schema->SQLFactoryClass()->new_select();

    $explicit_wiki_select->select(
        $Schema->table('Wiki')->column('wiki_id') );
    $class->_ExplicitWikiSelectBase($explicit_wiki_select);

    my $implicit_wiki_select = Silki::Schema->SQLFactoryClass()->new_select();

    $implicit_wiki_select->select(
        $Schema->table('Wiki')->column('wiki_id') );
    $class->_ImplicitWikiSelectBase($implicit_wiki_select);

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    my $distinct = Fey::Literal::Term->new(
        'DISTINCT ',
        $Schema->table('Wiki')->column('wiki_id')
    );
    my $count = Fey::Literal::Function->new( 'COUNT', $distinct );

    #<<<
    $select
        ->select($count)->from( $Schema->table('Wiki') )
        ->where( $Schema->table('Wiki')->column('wiki_id'),
                 'IN', $explicit_wiki_select )
        ->where('or')
        ->where( $Schema->table('Wiki')->column('wiki_id'),
                 'IN', $implicit_wiki_select );
    #>>>
    return $select;
}

sub _BuildAllWikiSelect {
    my $class = shift;

    my $explicit_wiki_select = Silki::Schema->SQLFactoryClass()->new_select();

    my $is_explicit1 = Fey::Literal::Term->new('1 AS is_explicit');
    $is_explicit1->set_can_have_alias(0);
    $explicit_wiki_select->select( $Schema->table('Wiki'), $is_explicit1 );
    $class->_ExplicitWikiSelectBase($explicit_wiki_select);

    my $implicit_wiki_select = Silki::Schema->SQLFactoryClass()->new_select();

    my $is_explicit0 = Fey::Literal::Term->new('0 AS is_explicit');
    $is_explicit0->set_can_have_alias(0);
    $implicit_wiki_select->select( $Schema->table('Wiki'), $is_explicit0 );
    $class->_ImplicitWikiSelectBase($implicit_wiki_select);

    my $union = Silki::Schema->SQLFactoryClass()->new_union;

    # To use an ORDER BY with a UNION in Pg, you specify the column as a
    # number (ORDER BY 5).
    my $is_explicit_idx = ( scalar $Schema->table('Wiki')->columns() ) + 1;

    my $title_idx = first_index { $_->name() eq 'title' }
    $Schema->table('Wiki')->columns();

    $union->union( $explicit_wiki_select, $implicit_wiki_select )->order_by(
        Fey::Literal::Term->new($is_explicit_idx),
        'DESC',
        Fey::Literal::Term->new($title_idx),
        'ASC',
    );

    return $union;
}

sub _ExplicitWikiSelectBase {
    my $class  = shift;
    my $select = shift;

    $select->from( $Schema->tables( 'Wiki', 'UserWikiRole' ) )->where(
        $Schema->table('UserWikiRole')->column('user_id'),
        '=', Fey::Placeholder->new()
    );

    return;
}

sub _ImplicitWikiSelectBase {
    my $class  = shift;
    my $select = shift;

    my $explicit = Silki::Schema->SQLFactoryClass()->new_select();
    $explicit->select( $Schema->table('Wiki')->column('wiki_id') );
    $class->_ExplicitWikiSelectBase($explicit);
    #<<<
    $select
        ->from( $Schema->tables( 'Wiki', 'Page' ) )
        ->from( $Schema->tables( 'Page', 'PageRevision' ) )
        ->where( $Schema->table('PageRevision')->column('user_id'),
                 '=', Fey::Placeholder->new() )
        ->and( $Schema->table('Wiki')->column('wiki_id'),
               'NOT IN', $explicit );
    #>>>
    return;
}

sub recently_viewed_pages {
    my $self = shift;
    my ( $limit, $offset ) = validated_list(
        \@_,
        limit  => { isa => Int, optional => 1 },
        offset => { isa => Int, default  => 0 },
    );

    my $select = $self->_RecentlyViewedPagesSelect()->clone();
    $select->limit( $limit, $offset );

    return Fey::Object::Iterator::FromSelect->new(
        classes => ['Silki::Schema::Page'],
        select  => $select,
        dbh => Silki::Schema->DBIManager()->source_for_sql($select)->dbh(),
        bind_params => [ $self->user_id() ],
    );
}

sub _BuildRecentlyViewedPagesSelect {
    my $class = shift;

    my ( $page_t, $page_view_t ) = $Schema->tables( 'Page', 'PageView' );

    my $max_func = Fey::Literal::Function->new(
        'MAX',
        $page_view_t->column('view_datetime')
    );

    my $max_datetime = Silki::Schema->SQLFactoryClass()->new_select();

    #<<<
    $max_datetime
        ->select($max_func)
        ->from( $page_view_t )
        ->where( $page_view_t->column('page_id'),
                 '=', $page_t->column('page_id') );
    #>>>
    my $viewed_select = Silki::Schema->SQLFactoryClass()->new_select();
    #<<<
    $viewed_select
        ->select($page_t)
        ->from( $page_t, $page_view_t )
        ->where( $page_view_t->column('user_id'), '=', Fey::Placeholder->new() )
        ->and  ( $page_view_t->column('view_datetime') , '=', $max_datetime )
        ->order_by(
            $page_view_t->column('view_datetime'), 'DESC',
            $page_t->column('title'),              'ASC',
        );
    #>>>
    return $viewed_select;
}

sub send_invitation_email {
    my $self = shift;

    $self->_send_email( @_, template => 'invitation' );
}

sub send_activation_email {
    my $self = shift;

    $self->_send_email( @_, template => 'activation' );
}

sub forgot_password {
    my $self = shift;

    $self->update(
        confirmation_key =>
            $self->_make_confirmation_key( $self->email_address() ),
        user => Silki::Schema::User->SystemUser(),
    );

    $self->_send_email(
        @_,
        sender   => $self,
        subject  => loc('Password reset for Silki'),
        template => 'forgot-password',
    );
}

sub _send_email {
    my $self = shift;
    my ( $wiki, $sender, $domain, $message, $subject, $template )
        = validated_list(
        \@_,
        wiki   => { isa => 'Silki::Schema::Wiki', optional => 1 },
        sender => { isa => 'Silki::Schema::User' },
        domain => {
            isa     => 'Silki::Schema::Domain',
            default => Silki::Schema::Domain->DefaultDomain()
        },
        message  => { isa => Str, optional => 1 },
        subject  => { isa => Str, optional => 1 },
        template => { isa => Str },
        );

    die "Cannot send an invitation email without a wiki."
        if $template eq 'invitation' && !$wiki;

    $subject ||=
        $wiki
        ? loc(
        'You have been invited to join the %1 wiki at %2',
        $wiki->title(),
        $wiki->domain()->web_hostname(),
        )
        : loc(
        'Activate your user account on the %1 server',
        $self->domain()->web_hostname()
        );

    my $from = Email::Address->new(
        $sender->best_name(),
        $sender->email_address()
    )->format();

    my $to = Email::Address->new(
        $self->best_name(),
        $self->email_address(),
    )->format();

    send_email(
        from            => $from,
        subject         => $subject,
        to              => $to,
        template        => $template,
        template_params => {
            user    => $self,
            wiki    => $wiki,
            domain  => $domain,
            sender  => $sender,
            message => $message,
        },
    );

    return;
}

sub ActiveUsers {
    my $class = shift;
    my ( $limit, $offset ) = validated_list(
        \@_,
        limit  => { isa => Int, optional => 1 },
        offset => { isa => Int, default  => 0 },
    );

    my $select = $class->_ActiveUsersSelect()->clone();
    $select->limit( $limit, $offset );

    my $dbh = Silki::Schema->DBIManager()->source_for_sql($select)->dbh();

    return Fey::Object::Iterator::FromSelect->new(
        classes     => 'Silki::Schema::User',
        select      => $select,
        dbh         => $dbh,
        bind_params => [ $select->bind_params() ],
    );
}

sub _BuildActiveUsersSelect {
    my $class = shift;

    my $select = $class->_AllUsersSelect()->clone();

    my $user_t = $Schema->table('User');

    $select->where( $user_t->column('is_disabled'), '=', 0 );

    return $select;
}

sub ActiveUserCount {
    my $class = shift;

    my $select = $class->_ActiveUserCountSelect();

    my $dbh = Silki::Schema->DBIManager()->source_for_sql($select)->dbh();

    my $vals = $dbh->selectrow_arrayref(
        $select->sql($dbh), {},
        $select->bind_params()
    );

    return $vals ? $vals->[0] : 0;
}

sub _BuildActiveUserCountSelect {
    my $class = shift;

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    my $user_t = $Schema->table('User');

    my $count
        = Fey::Literal::Function->new( 'COUNT', $user_t->column('user_id') );

    #<<<
    $select
        ->select($count)
        ->from($user_t)
        ->where( $user_t->column('is_disabled'), '=', 0 );
    #>>>
    return $select;
}

sub All {
    my $class = shift;
    my ( $limit, $offset ) = validated_list(
        \@_,
        limit  => { isa => Int, optional => 1 },
        offset => { isa => Int, default  => 0 },
    );

    my $select = $class->_AllUsersSelect()->clone();
    $select->limit( $limit, $offset );

    my $dbh = Silki::Schema->DBIManager()->source_for_sql($select)->dbh();

    return Fey::Object::Iterator::FromSelect->new(
        classes     => 'Silki::Schema::User',
        select      => $select,
        dbh         => $dbh,
        bind_params => [ $select->bind_params() ],
    );
}

sub _BuildAllUsersSelect {
    my $class = shift;

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    my $user_t = $Schema->table('User');

    my $order_by = Fey::Literal::Term->new(
        q{CASE WHEN display_name = '' THEN username ELSE display_name END});

    #<<<
    $select
        ->select($user_t)
        ->from($user_t)
        ->order_by($order_by);
    #>>>
    return $select;
}

sub _all_revisions_for_delete {
    my $self = shift;

    my $select = $self->_AllRevisionsForDeleteSelect();

    my $dbh = Silki::Schema->DBIManager()->source_for_sql($select)->dbh();

    return Fey::Object::Iterator::FromSelect->new(
        classes     => 'Silki::Schema::PageRevision',
        select      => $select,
        dbh         => $dbh,
        bind_params => [ $self->user_id() ],
    );
}

sub _BuildAllRevisionsForDeleteSelect {
    my $class = shift;

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    my $rev_t = $Schema->table('PageRevision');

    #<<<
    $select
        ->select($rev_t)
        ->from($rev_t)
        ->where( $rev_t->column('user_id'), '=', Fey::Placeholder->new() )
        ->order_by( $rev_t->column('page_id'), 'ASC',
                    $rev_t->column('revision_number'), 'DESC',
                  );
    #>>>
    return $select;
}

sub _BuildAllPageCountSelect {
    my $class = shift;

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    my $page_t = $Schema->table('Page');

    my $count
        = Fey::Literal::Function->new( 'COUNT', $page_t->column('page_id') );

    #<<<
    $select
        ->select($count)
        ->from($page_t)
        ->where( $page_t->column('user_id'), '=', Fey::Placeholder->new() );
    #>>>
    return $select;
}

sub _BuildAllRevisionCountSelect {
    my $class = shift;

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    my $rev_t = $Schema->table('PageRevision');

    my $count
        = Fey::Literal::Function->new( 'COUNT', $rev_t->column('page_id') );

    #<<<
    $select
        ->select($count)
        ->from($rev_t)
        ->where( $rev_t->column('user_id'), '=', Fey::Placeholder->new() );
    #>>>
    return $select;
}

sub _BuildAllFileCountSelect {
    my $class = shift;

    my $select = Silki::Schema->SQLFactoryClass()->new_select();

    my $file_t = $Schema->table('File');

    my $count
        = Fey::Literal::Function->new( 'COUNT', $file_t->column('file_id') );

    #<<<
    $select
        ->select($count)
        ->from($file_t)
        ->where( $file_t->column('user_id'), '=', Fey::Placeholder->new() );
    #>>>
    return $select;
}

__PACKAGE__->meta()->make_immutable();

1;

# ABSTRACT: Represents a user

__END__
=pod

=head1 NAME

Silki::Schema::User - Represents a user

=head1 VERSION

version 0.29

=head1 AUTHOR

Dave Rolsky <autarch@urth.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2011 by Dave Rolsky.

This is free software, licensed under:

  The GNU Affero General Public License, Version 3, November 2007

=cut