# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: PageAlert.pm 3138 2008-06-17 21:19:10Z josh $

package BigMed::PageAlert;
use strict;
use warnings;
use utf8;

use base qw(BigMed::Data);
use BigMed;
use BigMed::Email qw(send_email);
use Carp;
$Carp::Verbose = 1;

###########################################################
# SET DATA SCHEMA
###########################################################

my @data_schema = (
    {   name  => 'alert',
        type  => 'simple_text',
        index => 1,
    },
    {   name  => 'user',
        type  => 'id',
        index => 1,
    },
    {   name  => 'site',
        type  => 'system_id',    #zero-plus
        index => 1,
    },
    {   name     => 'sections',
        type     => 'system_id',    #zero-plus
        multiple => 1,
    },
    {   name => 'mine',
        type => 'boolean',
    },
);

BigMed::PageAlert->set_schema(
    elements   => \@data_schema,
    systemwide => 1,
);

###########################################################
# REGISTER ALERT TYPES
###########################################################

my $DEFAULT_MIN_LEVEL = 2;    #writer
my %ALERT= (
    'page_new' => {
        min_level => 3,                   #editor
        callback  => \&_alert_new_page,
    },
    'page_status' => { callback => \&_alert_status_change, },
    'comment_mod'   => {
        min_level => 4,                      #publisher
        callback  => \&_alert_mod_comment,
    },
    'comment_new' => { callback => \&_alert_new_comment, },
);

sub register_page_alert {
    my $self = shift;
    %ALERT = ( %ALERT, @_ );
    return;
}

sub alert_types {
    my ( $self, $level ) = @_;
    my @types = grep {
        !$level || $level >= ( $ALERT{$_}->{min_level} || $DEFAULT_MIN_LEVEL )
    } sort keys %ALERT;
    return @types;
}

sub min_level {
    my ($self, $name) = @_;
    $name = $self->alert if !$name && ref $self;
    croak 'no name argument' if !$name;
    return $ALERT{$name}->{min_level} || $DEFAULT_MIN_LEVEL;
}

sub notify {
    my $self = shift;
    my $name = shift;
    croak "No page alert '$name' registered" if !$ALERT{$name};

    my $page     = $_[0];
    my $site_id  = $page->site;
    my @sections = $page->sections;
    my %sec      = map { $_ => 1 } @sections;
    my $owner    = $page->owner || 0;

    my $class = ref $self || $self;
    my $select = $class->select( { alert => $name, site => [0, $site_id] } )
      or return;
    my @alerts;
    my $obj;
    my $min_level = $ALERT{$name}->{min_level} || $DEFAULT_MIN_LEVEL;
    while ( $obj = $select->next ) {
        if (scalar ($obj->sections) ) {
            my $in_sec;
            foreach my $sec_id ($obj->sections ) {
                $in_sec = 1, last if $sec{$sec_id};
            }
            next if !$in_sec;
        }
        my $uid = $obj->user;
        next if $obj->mine && $uid != $owner;

        my $user = BigMed::User->fetch($uid) or next;
        next if $user->privilege_level($site_id) < $min_level;
        push @alerts, [$obj, $user];
    }
    return   if !defined $obj;
    return 1 if !@alerts;

    return $ALERT{$name}->{callback}->( $self, \@alerts, @_ );
}

sub _alert_new_page {
    return _general_page_alert( @_[0 .. 3], 'New Page' );
}

sub _alert_status_change {
    return _general_page_alert( @_[0 .. 3], 'Status Change' );
}

sub _alert_mod_comment {
    return _general_comment_alert( @_[0 .. 4], 'Moderate Comment' );
}

sub _alert_new_comment {
    return _general_comment_alert( @_[0 .. 4], 'New Comment' );
}

sub _general_page_alert {
    my ( $self, $ralerts, $page, $site, $action ) = @_;

    #prep header info
    my $bm        = BigMed->bigmed;
    my $from      = $bm->env('ADMINEMAIL');
    my $from_name = $bm->language('BM_bm administrator');
    my ( $page_title, $page_desc, $owner_name, $sec_text ) =
      _page_text_fields( $page, $site );
    my $site_name = _html_to_plain( $site->name );
    my $subject   =
      $bm->language( ["PAGEALERT_SUBJ_$action", $site_name, $page_title] );

    #message text
    my $intro       = $bm->language(["PAGEALERT_INTRO_$action", $site_name]);
    my $page_owner  = $bm->language('PAGEALERT_Owner') . ": $owner_name";
    my $sections    = $bm->language('PAGEALERT_Section(s)') . ":\n$sec_text";
    my $page_status =
        $bm->language('PAGEALERT_Status') . ': '
      . $bm->language( 'CONTENT_pub_status_' . $page->pub_status );
    my ( $edit, $view ) = _edit_and_view_urls($page, $site);
    my $edit_text = $bm->language('PAGEALERT_Edit this page') . ":\n$edit";
    my $view_text = $bm->language('PAGEALERT_View this page') . ":\n$view";
    my $pathdiv = $bm->env('USE_BMQUERY') ? q{?} : q{/};
    my $sid = $site->id;
    my $pref_text =
        "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n"
      . $bm->language('PAGEALERT_Edit prefs') . "\n"
      . $bm->env('MOXIEBIN')
      . "/bm-account.cgi${pathdiv}edit/$sid/";

    my $msg_top = <<"NEWPAGEMSG";
$intro

--------------------------------
$page_title

$page_status

$page_desc

$sections

$page_owner
--------------------------------

NEWPAGEMSG

    my %sent;
    foreach my $robj_user ( @{$ralerts} ) {
        my ( $obj, $user ) = @{$robj_user};
        my $to_mail = $user->email or next;
        next if $sent{ $user->email };
        my $message = $msg_top;
        if ( $page->editable_by_user($user) ) {
            $message .= "$edit_text\n\n";
        }
        $message .= "$view_text\n\n$pref_text" . $user->id . "\n";

        send_email(
            to        => $to_mail,
            from      => $from,
            from_name => $from_name,
            subject   => $subject,
            body      => $message,
        );
    }
    return 1;
}

sub _general_comment_alert {
    my ( $self, $ralerts, $page, $site, $comment, $action ) = @_;
BigMed::Log->log('info'=>'general comment alert triggered');
    #prep header info
    my $bm        = BigMed->bigmed;
    my $from      = $bm->env('ADMINEMAIL');
    my $from_name = $bm->language('BM_bm administrator');

    my $site_title = _html_to_plain( $site->name );
    my $page_title = _html_to_plain( $page->title );
    my $commenter  = _html_to_plain( $comment->commenter );
    my $content    =
      ( BigMed::Filter->extract_filter_and_text( $comment->content ) )[1];
    my $site_name = _html_to_plain( $site->name );
    my $subject   =
      $bm->language( ["PAGEALERT_SUBJ_$action", $site_title, $page_title] );

    #message text
    my $intro = $bm->language(["PAGEALERT_INTRO_$action", $site_title]);
    my ( $edit, $view ) = _edit_and_view_urls($page, $site);
    my $contrib = $bm->language('PAGEALERT_Contributor') . ": $commenter";
    if ($comment->email) {
        $contrib .= "\n" . $comment->email;
    }
    if ($comment->url) {
        $contrib .= "\n" . $comment->url;
    }

    #edit links
    my $sid       = $site->id;
    my $cid       = $comment->id;
    my $pathdiv = $bm->env('USE_BMQUERY') ? q{?} : q{/};
    my $cedit_url =
      $bm->env('MOXIEBIN') . "/bm-mod.cgi${pathdiv}edit/$sid/$cid";
    my $cview_url = "$view#bmc$cid";
    my $edit_text =
      $bm->language('PAGEALERT_Edit this comment') . ":\n$cedit_url";
    if ( $action eq 'Moderate Comment' ) {
        my $mod_url = $bm->env('MOXIEBIN') . "/bm-mod.cgi${pathdiv}mod/$sid";
        $edit_text = $bm->language('PAGEALERT_Review all moderated comments')
          . ":\n$mod_url\n\n$edit_text";
    }
    elsif ( $action eq 'New Comment' ) {
        $edit_text =
          $bm->language('PAGEALERT_View this comment')
          . ":\n$cview_url\n\n$edit_text";
    }
    my $pref_text = "\n"
      . $bm->language('PAGEALERT_Edit prefs') . "\n"
      . $bm->env('MOXIEBIN')
      . "/bm-account.cgi${pathdiv}edit/$sid/";

    my $message = <<"NEWPAGEMSG";
$intro

--------------------------------
$page_title

$contrib

$content
--------------------------------

$edit_text

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
NEWPAGEMSG

    my %sent;
    foreach my $robj_user ( @{$ralerts} ) {
        my ( $obj, $user ) = @{$robj_user};
        my $to_mail = $user->email or next;
        next if $sent{ $user->email };
        send_email(
            to        => $to_mail,
            from      => $from,
            from_name => $from_name,
            subject   => $subject,
            body      => ( $message . $pref_text . $user->id . "\n" ),
        );
    }
    return 1;
}

sub _html_to_plain {
    my $string = shift;
    return q{} if !defined $string;
    $string =~ s/<[^>]+>//mgs;
    $string =~ s{&(\#?[xX]?(?:[0-9a-fA-F]+|\w+));}{
        local $_ = $1;
        /^amp$/i      ? '&'
          : /^quot$/i ? '"'
          : /^gt$/i   ? '>'
          : /^lt$/i   ? '<'
          : /^#(\d+)$/ ? chr($1)
          : /^#x([0-9a-f]+)$/i ? chr( hex($1) )
          : $_
    }msgex;
    return $string;
}

sub _edit_and_view_urls {
    my ($page,$site)    = @_;
    my $bm      = BigMed->bigmed;
    my $pathdiv = $bm->env('USE_BMQUERY') ? q{?} : q{/};
    my ( $sid, $pid ) = ( $page->site, $page->id );
    my $edit_url =
      $bm->env('MOXIEBIN') . "/bm-editor.cgi${pathdiv}edit/$sid/page/$pid";
    my $preview_url;
    if ( $page->pub_status eq 'published' ) {
        $preview_url = $page->active_page_url($site);
    }
    if ( !$preview_url ) {
        $preview_url =
          $bm->env('MOXIEBIN')
          . "/bm-editor.cgi${pathdiv}preview/$sid/page/$pid";
    }
    
    #not all email clients handle tildes very well
    $preview_url =~ s/~/\%7E/msg;
    $edit_url =~ s/~/\%7E/msg;

    return ( $edit_url, $preview_url );
}

sub _page_text_fields {
    my ( $page, $site ) = @_;
    my $page_title = _html_to_plain( $page->title );
    my $page_desc  =
         _html_to_plain( BigMed::Filter->filter( $page->description ) )
      || _html_to_plain( BigMed::Filter->filter( $page->content ) );
    $page_desc =~ s/\A\s+//ms;
    $page_desc =~ s/\s+\z//ms;
    $page_desc =~ s/\s+/ /msg;
    if ( length $page_desc > 300 ) {
        $page_desc = substr( $page_desc, 0, 300 ) . '...';
    }
    my $owner = $page->owner ? BigMed::User->fetch( $page->owner ) : q{};
    my $owner_name = $owner ? $owner->name : q{--};

    my @sections;
    foreach my $sid ( $page->sections ) {
        my $sec = $site->section_obj_by_id($sid) or next;
        push @sections, _html_to_plain( $sec->name );
    }
    my $sec_text = join( ', ', @sections );
    return ( $page_title, $page_desc, $owner_name, $sec_text );
}


1;

__END__

=head1 Name

=head2 BigMed::PageAlert

Data-backed framework for managing user-specific preferences for
page-based events like e-mail alerts.

=head1 Synopsis

    use BigMed::PageAlert;

    #register new alert types and associated callback routines
    BigMed::PageAlert->register_page_alert(
        'my_alert' => {
            min_level => 3,                     #editor users or higher
            callback  => \&my_alert_callback,
        },
        'another_alert' => { callback => \&another_alert_callback, },
    );

    #create alert object preference
    my $alert = BigMed::PageAlert->new();
    $alert->set_alert('my_alert');
    $alert->set_site( $site->id );         #site to send alerts
    $alert->set_sections( \@sections );    #optional: specific section ids
    $alert->set_user( $user->id );         #id of user who wants alerts
    $alert->set_mine(1);                   #only alert for pages owned by user
    $alert->save or die 'Could not save!';

    #send out alerts for a page
    BigMed::PageAlert->notify( 'my_alert', $page, @args );
    
    sub my_alert_callback {
        my ( $class, $ralert_user_info, $page, @args ) = @_;

        #step through PageAlert objects that match page's site/section/mine
        #preferences for this alert type
        
        foreach my $ralert_and_user ( @{$ralert_user_info} ) {
            my ($alert_obj, $user_obj) = @{ $ralert_and_user };
            # ... do stuff ...
        }
    }

=head1 Description

BigMed::PageAlert provides data-backed preference objects and a simple
framework for handling callbacks for page-based events. This is used,
for example, to send e-mail alerts for new pages, changes in page status,
new comments, etc.

BigMed::PageAlert is a subclass of BigMed::Data. In addition to the methods 
documented below, please see the BigMed::Data documentation for details about
topics including:

=over 4

=item Creating a new data object

=item Saving a data object

=item Finding and sorting saved data objects

=item Data access methods

=item Error handling

=back

=head1 Data Access Methods

BigMed::PageAlert objects hold the following pieces of data. They can be
accessed and set using the standard data access methods described in the
BigMed::Data documentation. See the L<"Searching and Sorting"> section below
for details on the data columns available to search and sort BigMed::PageAlert
objects.

=over 4

=item * id

The numeric ID of the page alert object

=item * alert

The type of alert (e.g. "page_new", "page_status", "comment_new",
"comment_mod").

=item * user

The id of the BigMed::User object with whom this preference is associated.
For the built-in email alerts, e-mails are sent to this user's email
address.

=item * site

The id of the BigMed::Site object whose pages to which this alert preference
applies. If set to 0, the preference applies to all of the sites to which
the user has privileges.

=item * sections

If supplied, the alert will be applied only to pages belonging to sections
with the BigMed::Section id(s) specified in this field.

=item * mine

Boolean value; if true, the alert will be applied only to pages owned by
the user.

=item * mod_time

The time when the object was last saved to disk, in UTC (Greenwich Mean)
time. The format: YYYY-MM-DD HH:MM:SS

=item * create_time

The time when the object was first saved to disk, in UTC (Greenwich Mean)
time. The format: YYYY-MM-DD HH:MM:SS

=back

=head1 C<< BigMed::PageAlert->notify >>

    BigMed::PageAlert->notify( 'page_new', $page, $site );

The C<alert> method triggers an alert. There are four built-in alerts,
all of which take three arguments:

=over 4

=item 1. The name of the alert

=item 2. The BigMed::Content::Page object triggering the alert.

=item 3. The associated BigMed::Site object for the page.

=back

The alert types are:

=head2 C<page_new>

This alert is triggered when a new page of any publication status is added
to the site. E-mail alerts are sent to all users subscribing to this event
with editor privileges (level 3) or better.

=head2 C<page_status>

This e-mail alert is triggered when a page changes status.

=head2 C<comment_new>

This e-mail alert is triggered when a comment is published to the site.

=head2 C<comment_mod>

This e-mail alert is triggered when a comment is submitted but held for
review. E-mail alerts are sent to all users subscribing to this event with
publisher privileges (level 4) or better.

=head1 Registering and Retrieving Alert Metadata

You can supplement the built-in alert types with your own alerts.

=head2 C<< BigMed::PageAlert->register_page_alert >>

    BigMed::PageAlert->register_page_alert(
        'my_alert' => {
            min_level => 3,                     #editor users or higher
            callback  => \&my_alert_callback,
        },
        'another_alert' => { callback => \&another_alert_callback, },
    );

Registers one or more alert types and their associated callback routines.
The argument should be a hash whose keys are the names of the alerts
to register and whose values are hash-reference metadata parameters for
the alerts. The parameters in the hash reference are:

=over 4

=item * callback

This required parameter is the routine to be called when the alert is
triggered. The routine receives two arguments, followed by the
arguments passed via C<alert> (not including the alert name). These
two arguments are the BigMed::PageAlert class or object and an
array reference of array references containing alert/user pairs.

    BigMed::PageAlert->notify('my_alert', $page, $site);
    
    sub my_alert_callback {
        my ($self, $ralert_user_pairs, $page, $site) = @_;
        foreach my $ralert_user ( @{$ralert_user_pairs} ) {
            my ( $alert_obj, $user_obj ) = @{$ralert_user};
            # do stuff...
        }
    }

The C<alert> method always expects the second argument to be a
BigMed::Content::Page object, so your callback routine should always
expect to receive the page object as the third argument.

=item * min_level

The minimum user privilege level required to receive this type of alert.
The default is 2 (writer), but you can specify higher values to further
restrict the alert.

=back

=head2 C<< BigMed::PageAlert->alert_types >>

Returns an alphabetized list of registered page alerts.
The method accepts an optional argument, the user privilege level, which
limits the returned values to the alert types available to users with that
privilege value (based on the C<min_level> attribute of the alert type).

    #no level argument returns all alert types
    @types = BigMed::PageAlert->alert_types();
    #comment_new, comment_mod, page_new, page_status

    #level argument returns all alert types for that privilege level
    @types = BigMed::PageAlert->alert_types(2); #writer
    #comment_new, page_status
    
=head2 C<< BigMed::PageAlert->min_level >>

    my $level = BigMed::PageAlert->min_level('page_new'); # gets 3

Returns the min_level attribute of the alert type specified in the
first argument.

=head1 Author & Copyrights

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut
