# DynamicPreview - (c) 2014-2017 Juxtaposition. All Rights Reserved.
# This code cannot be redistributed without permission from juxtaposition.jp
# For more information, consult your DynamicPreview license.
#
package DynamicPreview::CMS;

use strict;
use warnings;
use utf8;
use Data::Dumper;
use URI;

use MT::Util;
use MIME::Base64;
use MIME::Types;
use HTTP::Status qw( status_message );
use File::Basename;

use constant TRACE => 1;

sub trace {
    my ($msg) = @_;
    print STDERR $msg if TRACE;
    return;
}

# CMS methods
sub show_preview {
    my $app = shift;
    my $param = {};
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';

    # $app->validate_magic or return;

    my $blog_id = $app->param('blog_id');
    my $blog = $app->blog;
    my $id = $app->param('id');
    my $entry_type = $app->param('_type') || 'entry';
    my @tmpl_ids = $app->multi_param('tmpl_id');
    my $cat_id = $app->param('cat_id');
    my $rn = $app->param('r');

    # entry または category があれば良い
    if($blog_id && ($id || $cat_id) && @tmpl_ids) {
        my ($entry, $categories);
        if($id) {
            require MT::Entry;
            $entry = MT::Entry->load({
                id => $id,
                blog_id => $blog_id,
            }) or return $app->errtrans('Invalid request.');
            $categories = $entry->categories;
            unless(@$categories) {
                require MT::Category;
                my $cat = MT::Category->load(
                    {
                        blog_id => $blog_id,
                    },
                    {
                        sort      => 'id',
                        direction => 'ascend',
                    }
                );
                unless ($cat) {
                    $cat = new MT::Category;
                    $cat->label( $app->translate("Preview") );
                    $cat->basename("preview");
                    $cat->parent(0);
                }
                $categories = [ $cat ];
            }
        }
        elsif($cat_id) {
            require MT::Category;
            my $cat = MT::Category->load($cat_id)
                or return $app->errtrans('Invalid request');
            $categories = [ $cat ];
        }

        $param->{can_edit_entry} = $app->user && $entry ? $app->user->can_edit_entry($entry) : 0;

        require MT::Template;
        require MT::TemplateMap;
        my @tmpl = MT::Template->load({
            id => \@tmpl_ids,
        }, {
            sort => 'name',
            direction => 'ascend',
        });
        return $app->errtrans('Invalid request.') unless @tmpl > 0;

        my @items = ();
        foreach my $tmpl (@tmpl) {
            my $item = {
                id => $tmpl->id,
                name => $tmpl->name,
                blog_id => $tmpl->blog_id,
                type => $tmpl->type,
                magic_token => $app->current_magic,
                query => undef,
            };
            my $type = lc $tmpl->type;
            if($entry && $type eq 'individual' || $type eq 'page') {
                my $query = $app->uri_params(
                    mode => 'dynamic_preview_entry',
                    args => {
                        id => $entry->id,
                        blog_id => $entry->blog_id,
                        _type => $entry_type,
                        tmpl_id => $tmpl->id,
                        magic_token => $app->current_magic,
                        ( $rn ? ( r => $rn ) : () ),
                    },
                );
                $query =~ s!^\?!!;
                $item->{query} = $query;
                push @items, $item;
            }
            elsif($type eq 'archive') {
                my @map = MT::TemplateMap->load({
                    template_id => $tmpl->id,
                    build_type => { op => '!=', value => 0 },
                    archive_type => 'Category',
                });
                if(@map) {
                    foreach my $map (@map) {
                        my $new_item = { %$item };
                        $new_item->{archive_type} = lc $map->archive_type;
                        if ($map->archive_type =~ m!category!i) {
                            foreach (@$categories) {
                                my $new_item = { %$item };
                                $new_item->{cat_id} = $_->id;
                                $new_item->{cat_label} = $_->label;
                                push @items, $new_item;
                            }
                        }
                        else {
                            push @items, $new_item;
                        }
                    }
                }
                else {
                    push @items, $item;
                }
            }
            elsif($type eq 'index') {
                push @items, $item;
            }
        }
        $param->{entry_type} = $entry ? ($entry->class_type || 'entry') : '';
        $param->{item_loop} = \@items;
        $param->{from_external_link} = 1;
        my $idx = 0;
        my %sort_order = map { $_ => $idx++ } @tmpl_ids;
        $param->{sort_order} = \%sort_order;
    }

    $param->{id} = $id;
    $param->{entry_type} = $entry_type unless $param->{entry_type};
    $param->{cat_id} = $cat_id;
    $param->{blog_id} = $blog_id;
    $param->{tmpl_id} = @tmpl_ids ? \@tmpl_ids : undef;
    $param->{preview_url} = $app->mt_uri;
    $param->{admin_cgi_path} = _admin_cgi_path($app);
    $param->{admin_script} = _build_tag($app, '<mt:AdminScript>');
    $param->{has_extra} = $plugin->has_extra;
    $param->{dirty_entry} = $app->param('dirty');
    $param->{rev_number} = defined($rn) ? $rn : undef;
    $param->{admin_theme_id} = $app->config('AdminThemeId');
    my $tmpl = $plugin->load_tmpl('show.tmpl', $param);
    return $tmpl;
}

sub _admin_cgi_path {
    my $app = shift;
    my $host = $ENV{SERVER_NAME} || $ENV{HTTP_HOST} || 'localhost';
    if($ENV{HTTP_X_FORWARDED_HOST}) {
        $host = (split ',', $ENV{HTTP_X_FORWARDED_HOST})[-1];
    }
    my $tmpl_str = '<mt:AdminCGIPath>';
    my $path = _build_tag($app, $tmpl_str);
    my $uri = URI->new($path);
    if($uri->host ne $host) {
        $uri->host($host);
    }
    return $uri->as_string;
}

sub _build_tag {
    my $app = shift;
    my $tag_str = shift;
    my $param = shift || {};
    return MT::build_page($app, \$tag_str, $param);
}

sub _get_cd_categories {
    my ($cd, $cat_field_id) = @_;
    my $ct = $cd->content_type;
    my $cat_fields = $ct->categories_fields;
    my @categories;
    foreach my $field (@$cat_fields) {
        if ($cat_field_id && $field->{id} != $cat_field_id) {
            next;
        }
        my $cat_ids = $cd->data->{ $field->{id} };
        if (!$cat_ids) {
            next;
        }
        my @cats = MT::Category->load({ id => @$cat_ids });
        push @categories, @cats;
    }
    return @categories;
}

sub _get_primary_category {
    my ($cd, $map) = @_;
    my $obj_cat = MT->model('objectcategory')->load({
        cf_id => $map->cat_field_id,
        is_primary => 1,
        object_ds => 'content_data',
        object_id => $cd->id,
    }) or return;
    my $cat = MT->model('category')->load($obj_cat->category_id);
    return $cat;
}

# Preview用のデータを取得する
# -> 編集画面のメニュー用とPreview画面のタブ用にデータを取得する
sub _get_preview_cd_items {
    my $app = shift;
    my ($content_data, $blog_id, $tmpl_ids) = @_;

    if(! ref $content_data) {
        $content_data = MT->model('content_data')->load({
            id => $content_data,
            blog_id => $blog_id,
        }) or return;
    }

    my @tmpl = MT->model('template')->load(
        { id => $tmpl_ids },
        { sort => 'name', direction => 'ascend' }
    ) or return;

    my @items = ();

    foreach my $tmpl (@tmpl) {
        # exclude is template is not associated with content type
        # next if $tmpl->content_type_id && $content_data->content_type_id
        #     && $tmpl->content_type_id != $content_data->content_type_id;

        my $item = {
            id => $tmpl->id,
            name => $tmpl->name,
            blog_id => $tmpl->blog_id,
            type => $tmpl->type,
            magic_token => $app->current_magic,
            query => undef,
            cd_id => $content_data->id,
        };
        my $type = lc $tmpl->type;
        if($type eq 'ct') {
            my $query = $app->uri_params(
                mode => 'dynamic_preview_content_data',
                args => {
                    id => $content_data->id,
                    content_type_id => $content_data->content_type_id,
                    blog_id => $content_data->blog_id,
                    _type => $app->param('_type') || 'content_data',
                    tmpl_id => $tmpl->id,
                    magic_token => $app->current_magic,
                    from_external_link => 1,
                    ( $app->param('r') ? ( r => $app->param('r') ) : () ),
                },
            );
            $query =~ s!^\?!!;
            $item->{query} = $query;
            push @items, $item;
        }
        elsif($type eq 'ct_archive') {
            # template mapの分だけループして$itemを追加する
            my @maps = MT::TemplateMap->load({
                template_id => $tmpl->id,
                build_type => { op => '!=', value => 0 },
            });
            if(@maps) {
                foreach my $map (@maps) {
                    my $archiver = $app->publisher->archiver($map->archive_type);
                    $item->{archive_label} = $archiver->archive_short_label;
                    if($map->archive_type =~ /category/i) {
                        # category based archiveの場合は、categoryごとに$itemを追加する
                        # template mapに紐付けられたcategoryのみを取得
                        my @categories = _get_cd_categories($content_data, $map->cat_field_id);
                        foreach my $cat (@categories) {
                            my $new_item = { %$item };
                            $new_item->{archive_type} = lc $map->archive_type;
                            $new_item->{cat_id} = $cat->id;
                            $new_item->{cat_label} = $cat->label;
                            $new_item->{map_id} = $map->id;
                            push @items, $new_item;
                        }
                    }
                    else {
                        my $new_item = { %$item };
                        $new_item->{archive_type} = lc $map->archive_type;
                        my $pcat = _get_primary_category($content_data, $map);
                        if($pcat) {
                            $new_item->{cat_id} = $pcat->id;
                            $new_item->{cat_label} = $pcat->label;
                        }
                        $new_item->{map_id} = $map->id;
                        push @items, $new_item;
                    }
                }
            }
            else {
                push @items, $item;
            }
        }
        elsif($type eq 'index') {
            push @items, $item;
        }
    }

    return @items;
}

sub show_preview_cd {
    my $app = shift;
    my $param = {};
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';

    # $app->validate_magic or return;
    my $blog_id = $app->param('blog_id');
    my $blog = $app->blog;
    my $id = $app->param('id');
    my $obj_type = $app->param('_type') || 'content_data';
    my @tmpl_ids = $app->multi_param('tmpl_id');
    my $rn = $app->param('r');
    my $content_type_id = $app->param('content_type_id');

    if($blog_id && $id && @tmpl_ids) {
        my $content_data = MT->model('content_data')->load({
            id => $id,
            blog_id => $blog_id,
        }) or return $app->errtrans('Invalid request.');

        $param->{can_edit_content_data} = $app->user && $content_data 
            ? $app->user->permissions($blog_id)->can_edit_content_data($content_data, $app->user)
            : 0;

        my @items = _get_preview_cd_items($app, $content_data, $blog_id, \@tmpl_ids)
            or return $app->errtrans('Invalid request.');
        $param->{item_loop} = \@items;
        $param->{obj_type} = 'content_data';
        $param->{from_external_link} = 1;
        my $idx = 0;
        my %sort_order = map { $_ => $idx++ } @tmpl_ids;
        $param->{sort_order} = \%sort_order;
    }

    $param->{id} = $id;
    $param->{obj_type} = $obj_type unless $param->{obj_type};
    $param->{blog_id} = $blog_id;
    $param->{content_type_id} = $content_type_id;
    $param->{tmpl_id} = @tmpl_ids ? \@tmpl_ids : undef;
    $param->{preview_url} = $app->mt_uri;
    $param->{admin_cgi_path} = _admin_cgi_path($app);
    $param->{admin_script} = _build_tag($app, '<mt:AdminScript>');
    $param->{has_extra} = $plugin->has_extra;
    $param->{dirty_obj} = $app->param('dirty');
    $param->{rev_number} = defined($rn) ? $rn : undef;
    $param->{admin_theme_id} = $app->config('AdminThemeId');
    my $tmpl = $plugin->load_tmpl('show_cd.tmpl', $param);
    return $tmpl;
}

sub _load_users {
    my $app = shift;
    my ($entry, $category, $content_data) = @_;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';
    return (wantarray ? () : undef) unless $entry || $category || $content_data;

    my $blog_id = $app->param('blog_id');

    require MT::Author;
    my @authors;
    my $iter = MT::Author->load_iter();
    while(my $author = $iter->()) {
        my $perms = $author->permissions($blog_id)
            or next;
        if($author->is_superuser
            || $author->can_do('preview', at_least_one => 1, blog_id => $blog_id)
            || ($entry && $author->can_edit_entry($entry))
            || ($category && $author->can_edit_templates())
            || ($content_data && $perms->can_edit_content_data($content_data, $author))) {
            push @authors, $author;
        }
    }
    return wantarray ? @authors : $authors[0];
}

sub edit_mail {
    my $app = shift;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';
    return $app->error(
        $app->translate('No permissions.')
    ) unless $plugin->has_extra;

    my $blog_id = $app->param('blog_id');
    my $blog = $app->blog;
    my $preview_url = $app->param('preview_url');
    my $type = $app->param('_type') || 'entry';
    my $entry_id = $app->param('entry_id');
    my $cat_id = $app->param('cat_id');
    my $content_data_id = $app->param('content_data_id');
    my $content_type_id = undef;
    if($type eq 'entry' && !$entry_id && !$cat_id) {
        return $app->error( $app->translate('Entry ID or category ID was not provided') );
    }
    elsif($type eq 'content_data' && !$content_data_id) {
        return $app->error($app->translate('Content Data ID was not provided'));
    }

    my ($entry, $category, $content_data, $content_type);

    if($entry_id) {
        require MT::Entry;
        $entry = MT::Entry->load($entry_id)
            or return $app->error(
                $app->translate("No such entry '[_1]'", $entry_id)
            );
    }
    if($cat_id) {
        require MT::Category;
        $category = MT::Category->load($cat_id)
            or return $app->error(
                $app->translate("No such category '[_1]'", $cat_id)
            );
    }
    if($content_data_id) {
        require MT::ContentData;
        $content_data = MT::ContentData->load($content_data_id)
            or return $app->error(
                $app->translate("No such content data '[_1]'", $content_data_id)
            );
        $content_type = $content_data->content_type;
        $content_type_id = $content_type->id;
    }

    my @users = _load_users($app, $entry, $category, $content_data);

    my $subject;
    my $subj_tmpl_str = $plugin->dynamic_preview_mail_subject($blog_id);
    if($subj_tmpl_str) {
        my $subj_tmpl = MT::Template->new(type => 'scalarref', source => \$subj_tmpl_str);
        $app->set_default_tmpl_params($subj_tmpl);
        my $ctx = $subj_tmpl->context;
        $ctx->stash('blog', $blog);
        my $entries = [];
        push @$entries, $entry if $entry;
        $ctx->stash('entries', $entries);
        $ctx->stash('entry', $entry);
        $ctx->stash('category', $category);
        $ctx->stash('content', $content_data);
        $ctx->stash('content_type', $content_type);
        $subject = $subj_tmpl->output({
            blog => $blog,
            entry => $entry,
            category => $category,
            content => $content_data,
            content_type => $content_type,
            type => $type,
        });
        if(!$subject && $subj_tmpl->errstr) {
            return $app->error($subj_tmpl->errstr);
        }
    }
    unless($subject) {
        my $label = $type eq 'entry' 
            ? $entry ? $entry->title : $category ? $category->label : ''
            : $content_data->label;
        $subject = $app->translate(
            "([_1]) Request for check - [_2]", $blog->name, $label
        );
    }

    my $param = {
        entry_id => $entry_id,
        preview_url => $preview_url,
        users => \@users,
        subject => $subject,
        cat_id => $cat_id,
        content_data_id => $content_data_id,
        content_type_id => $content_type_id,
        type => $type,
        admin_theme_id => $app->config('AdminThemeId'),
    };
    my $tmpl = $plugin->load_tmpl('edit.tmpl', $param);
    return $tmpl;
}

use MT::Util qw( is_valid_email is_url dirify );
use MT::I18N qw( wrap_text );

sub send_mail {
    my $app = shift;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';
    return $app->error(
        $app->translate('No permissions.')
    ) unless $plugin->has_extra;
    $app->validate_magic or return;

    my $type = $app->param('_type') || 'entry';
    my $entry_id = $app->param('entry_id');
    my $cat_id = $app->param('cat_id');
    my $content_data_id = $app->param('content_data_id');
    my $content_type_id = undef;
    if($type eq 'entry' && !$entry_id && !$cat_id) {
        return $app->error( $app->translate('Entry ID or category ID was not provided') );
    }
    elsif($type eq 'content_data' && !$content_data_id) {
        return $app->error($app->translate('Content Data ID was not provided'));
    }

    my ($entry, $category, $blog, $content_data, $content_type);

    if($entry_id) {
        require MT::Entry;
        $entry = MT::Entry->load($entry_id)
            or return $app->error(
                $app->translate("No such entry '[_1]'", $entry_id)
            );
        $blog = $entry->blog;
    }
    if($cat_id) {
        require MT::Category;
        $category = MT::Category->load($cat_id)
            or return $app->error(
                $app->translate("No such category '[_1]'", $cat_id)
            );
        $blog = MT::Blog->load($category->blog_id);
    }
    if($content_data_id) {
        require MT::ContentData;
        $content_data = MT::ContentData->load($content_data_id)
            or return $app->error(
                $app->translate("No such content data '[_1]'", $content_data_id)
            );
        $blog = $content_data->blog;
        $content_type = $content_data->content_type;
        $content_type_id = $content_type->id;
    }

    my $user = $app->user;
    my $saved_app_blog = $app->blog;
    $app->blog($blog);

    # my $author = $entry ? $entry->author : undef;
    my $author = $entry ? $entry->author 
                        : $content_data ? $content_data->author 
                                        : $user;
    my $preview_url = $app->param('preview_url')
        or return $app->error(
            $app->translate('No preview URL was provided')
        );

    my $entries = [];
    push @$entries, $entry if $entry;
    my $params = {
        blog => $blog,
        entry => $entry,
        entries => $entries,
        category => $category,
        # entry_author => $author ? 1 : 0,
        entry_author => $entry ? 1 : 0,
        author => $author,
        has_author => $author ? 1 : 0,
        preview_url => $preview_url,
        content => $content_data,
        content_type => $content_type,
        type => $type,
    };

    my $cols = 72;
    my $message = $app->param('message');
    $params->{message} = wrap_text( $message, $cols, '', '');
    
    my $addrs;
    if($app->param('send_notify_emails')) {
        # my @addrs = split(/[\n\r,]+/, $app->param('send_notify_emails'));
        my @addrs = $app->multi_param('send_notify_emails');
        foreach my $addr (@addrs) {
            next unless is_valid_email($addr);
            $addrs->{$addr} = 1;
        }
    }

    keys %$addrs
        or return $app->error(
            $app->translate('No valid recipients were found for the entry notification.')
        );

    my $address;
    if($author) {
        $address = defined $author->nickname
            ? $author->nickname . ' <' . $author->email . '>'
            : $author->email;
    }
    else {
        $address = $app->config('EmailAddressMain');
        $params->{from_address} = $address;
    }

    my $tmpl_str = $plugin->dynamic_preview_mail_template($saved_app_blog->id)
        or return $app->error(
            $app->translate('No email template.')
        );
    my $body = build_mail_body($app, $tmpl_str, $params)
        or return;

    my $subject = $app->param('subject')
        or return $app->error(
            $app->translate('You must enter the subject.')
        );
    if($app->current_language ne 'ja') {
        $subject =~ s![\x80-\xFF]!!g;
    }
    my $header = {
        id => 'nofity_preview',
        To => $address,
        From => $address,
        Subject => $subject,
    };
    my $charset = $app->config('MailEncoding') || $app->charset;
    $header->{'Content-Type'} = qq!text/plain; charset="$charset"!;
    my $i = 1;
    require MT::Mail;
    unless(exists $params->{from_address}) {
        MT::Mail->send($header, $body)
            or return $app->errtrans(
                "Error sending mail ([_1]): Try another MailTransfer setting?",
                MT::Mail->errstr
            );
    }
    delete $header->{To};

    my @email_to_send;
    my @addresses_to_send = grep $_, keys %$addrs;
    if($app->config('EmailNotificationBcc')) {
        while(@addresses_to_send) {
            push @email_to_send, {
                %$header,
                Bcc => [ splice(@addresses_to_send, 0, 20) ], 
            }
        }
    }
    else {
        @email_to_send = map {
            { %$header, To => $_ }
        } @addresses_to_send;
    }

    foreach my $info (@email_to_send) {
        MT::Mail->send($info, $body)
            or return $app->errtrans(
                "Error sending mail ([_1]): Try another MailTransfer setting?",
                MT::Mail->errstr
            );
    }

    $app->blog($saved_app_blog);
    my $tmpl = $plugin->load_tmpl('send.tmpl', {
        admin_theme_id => $app->config('AdminThemeId'),
    });
    return $tmpl;
}

sub build_mail_body {
    my ($app, $tmpl_str, $params) = @_;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';
    my $tmpl = MT::Template->new(type => 'scalarref', source => \$tmpl_str);
    $app->set_default_tmpl_params($tmpl);
    my $blog = $params->{blog} || undef;
    my $ctx = $tmpl->context;
    $ctx->stash('blog_id', $blog->id) if $blog;
    foreach my $name (qw/blog entry author category entries content content_type/) {
        $ctx->stash($name, delete $params->{$name}) if $params->{$name};
    }
    return $app->build_page_in_mem($tmpl, $params);
}

sub _setup_category_ids_param {
    my ($app) = @_;

    my $type = $app->param('_type') || 'entry';
    my $entry_class = $app->model($type);
    my $blog_id = $app->param('blog_id');
    my $id = $app->param('id')
        or return $app->errtrans("Invalid request.");

    require MT::Entry;
    my $entry = MT::Entry->load({ id => $id, blog_id => $blog_id })
        or return $app->errtrans("Invalid request.");

    require MT::Placement;
    my @maps = MT::Placement->search( { entry_id => $entry->id } );
    my $sorter = sub {
        my ($a, $b) = @_;
        if($a->is_primary > $b->is_primary) {
            return -1;
        }
        elsif($a->is_primary < $b->is_primary) {
            return 1;
        }
        return $a->category_id <=> $b->category_id;
    };
    my $cat_ids = join ",", map { $_->category_id } sort { $sorter->($a, $b) } @maps;
    if($cat_ids) {
        $app->param('category_ids', $cat_ids);
    }
}

sub _create_temp_entry {
    my $app         = shift;

    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';

    my $type        = $app->param('_type') || 'entry';
    my $entry_class = $app->model($type);
    my $blog_id     = $app->param('blog_id');
    my $blog        = $app->blog;
    my $id          = $app->param('id');
    my $entry;
    my $user_id = $app->user->id;
    my $rn = $app->param('r');
    if(!$rn && $plugin->dynamic_preview_load_latest_revision) {
        my $rev = $plugin->latest_revision($type, $id);
        $rn = $rev->rev_number if $rev;
    }

    if ($id) {
        $entry = $entry_class->load( { id => $id, blog_id => $blog_id } )
            or return $app->errtrans("Invalid request.");
        if($rn && $blog->use_revision) {
            my $rev = $entry->load_revision({ rev_number => $rn });
            if($rev && @$rev) {
                $entry = $rev->[0];
            }
        }
        $user_id = $entry->author_id;
    }
    else {
        $entry = $entry_class->new;
        $entry->author_id($user_id);
        $entry->id(-1);    # fake out things like MT::Taggable::__load_tags
        $entry->blog_id($blog_id);
    }

    return $app->return_to_dashboard( permission => 1 )
        unless $app->permissions->can_edit_entry( $entry, $app->user );

    my $names = $entry->column_names;
    my %values = map { $_ => scalar $app->param($_) } @$names;
    delete $values{'id'} unless $app->param('id');
    ## Strip linefeed characters.
    for my $col (qw( text excerpt text_more keywords )) {
        $values{$col} =~ tr/\r//d if $values{$col};
    }
    $values{allow_comments} = 0
        if !defined( $values{allow_comments} )
        || $app->param('allow_comments') eq '';
    $values{allow_pings} = 0
        if !defined( $values{allow_pings} )
        || $app->param('allow_pings') eq '';
    $entry->set_values( \%values );

    return $entry;
}

sub preview_entry {
    my $app = shift;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';

    $app->validate_magic or return;

    my $type = $app->param('_type') || 'entry'; 
    my $perms = $app->permissions;
    my $org_permissions = $perms->permissions || '';
    my $permissions;
    if($perms->can_do('preview')) {
        $permissions = $org_permissions;
        if($type eq 'entry' && !$perms->can_do('edit_all_posts')) {
            $permissions .= ",'edit_all_posts'";
        } 
        elsif($type eq 'page' && !$perms->can_do('manage_pages')) {
            $permissions .= ",'manage_pages'";
        }
        $perms->permissions($permissions);
    }

    require MT::CMS::Entry;
    my $tmpl = eval {
        _setup_category_ids_param($app);
        MT::CMS::Entry::preview($app, @_);
    };
    if($@) {
        trace("preview_entry: err: $@");
    }

    if($permissions) {
        $perms->permissions($org_permissions);
        $permissions = undef;
    }

    if(!$tmpl && $app->{redirect}) {
        $app->{redirect} = undef;
        return $app->errtrans('No permissions');
    }
    return $tmpl;
}

sub preview_template {
    my $app = shift;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';

    my $perms = $app->permissions;
    my $org_permissions = $perms->permissions || '';
    my $permissions;
    if($perms->can_do('preview') && !$perms->can_do('edit_templates')) {
        $permissions = $org_permissions . ",'edit_templates'";
        $perms->permissions($permissions);
    }

    my $blog_id = $app->param('blog_id');
    MT->request('dynamic_preview_include_hold_entry',
        $plugin->dynamic_preview_include_hold_entry($blog_id));
    MT->request('dynamic_preview_include_future_entry',
        $plugin->dynamic_preview_include_future_entry($blog_id));

    my $id = $app->param('id');
    require MT::Template;
    my $tmpl = $id ? MT::Template->load($id) : undef;
    my $cat_id = $app->param('cat_id');
    my $archive_cat = $cat_id ? MT->model('category')->load($cat_id) : undef;
    no strict 'refs';
    no warnings 'redefine';
    my $saved_create_preview_content = \&MT::CMS::Template::create_preview_content;
    local *MT::CMS::Template::create_preview_content = sub {
        my ( $app, $blog, $type, $number, $cat ) = @_;
        my $join = {};
        if($cat_id) {
            $join = { 
                join => MT->model('placement')->join_on(
                    'entry_id',
                    {
                        category_id => $cat_id,
                    },
                ),
            };
        }
        my $blog_id = $blog->id;
        my $entry_class = $app->model($type);
        my @obj = $entry_class->load(
            {
                blog_id => $blog_id,
                status => MT::Entry::RELEASE(),
            },
            {
                limit => $number || 1,
                direction => 'descend',
                sort => 'authored_on',
                %$join,
            }
        );
        unless(@obj) {
            @obj = $saved_create_preview_content->($app, $blog, $type, $number, $cat); 
        }

        # HACK: archive_categoryをcms_pre_previewで差し替えるため一時的に保存する
        $app->request('dynamic_preview_archive_category', $archive_cat);

        return @obj;
    };

    require MT::CMS::Template;
    my $preview_tmpl =  eval {
        MT::CMS::Template::preview($app, @_);
    };
    if($@) {
        trace("preview_template: err: $@");
    }
    
    MT->request('dynamic_preview_include_hold_entry', undef);
    MT->request('dynamic_preview_include_future_entry', undef);

    if($permissions) {
        $perms->permissions($org_permissions);
        $permissions = undef;
    }
    
    if(!$preview_tmpl && $app->{redirect}) {
        $app->{redirect} = undef;
        return $app->errtrans('No permissions');
    }
    return $preview_tmpl;
}

sub _rebuild_file {
    my $mt = shift;
    my ($blog, $root_path, $map, $at, $ctx, $cond, $build_static, %args) = @_;

    my $archiver = $mt->archiver($at);
    my ($start, $end, $category, $author, $content_data);

    if ($archiver->contenttype_category_based) {
        $category = $args{Category};
        die MT->translate("[_1] archive type requries [_2] parameter",
            $archiver->archive_label, 'Category')
            unless $args{Category};
        $category= MT::Category->load($category)
            unless ref $category;
        $ctx->{__stash}{archive_category} = $category;
        $ctx->{__stash}{template_map} = $map;
        my $category_set = MT->model('category_set')->load($category->category_set_id);
        $ctx->{__stash}{category_set} = $category_set;
    }
    if ($archiver->contenttype_date_based) {
        $start = $args{StartDate};
        $end = $args{EndDate};
        die MT->translate("[_1] archive type requries [_2] parameter",
            $archiver->archive_label, 'StartDate')
            unless $args{StartDate};
        $ctx->{__stash}{template_map} = $map;
    }
    if ($archiver->contenttype_author_based) {
        $author = $args{Author};
        die MT->translate("[_1] archive type requries [_2] parameter",
            $archiver->archive_label, 'Author')
            unless $args{Author};
        $author = MT::Author->load($author)
            unless ref $author;
        $ctx->{__stash}{author} = $author;
        $ctx->{__stash}{template_map} = $map;
    }
    if ($archiver->contenttype_based
        || ($archiver->contenttype_group_based && $args{ContentData})) {
        $content_data = $args{ContentData};
        die MT->translate("[_1] archive type requries [_2] parameter",
            $archiver->archive_label, 'ContentData')
            unless $args{ContentData};
        $content_data = MT::ContentData->load($content_data)
            unless ref $content_data;
        my $content_type = MT::ContentType->load($content_data->content_type_id);
        $ctx->var('content_archive', 1);
        $ctx->{__stash}{content_type} = $content_type;
        if($archiver->contenttype_based) {
            $ctx->{__stash}{content} = $content_data;
        }
        $ctx->{__stash}{template_map} = $map;
    }

    local $ctx->{current_timestamp} = $start if $start;
    local $ctx->{current_timestamp_end} = $end if $end;

    $ctx->{__stash}{blog} = $blog;
    $ctx->{__stash}{local_blog_id} = $blog->id;

    my $base_url = $blog->archive_url;
    $base_url = $base_url . $map->{__saved_output_file};
    my $url = $base_url . $map->{__saved_output_file};
    $url =~ s{(?<!:)//+}{/}g;

    my $tmpl = MT::Template->load($map->template_id)
        or die "Template not found: " . $map->template_id . "\n";
    # print "Template: " . $tmpl->name . "\n";
    return 1 if $tmpl->type eq 'backup';

    $tmpl->context($ctx);

    if (my $tmpl_param = $archiver->template_params) {
        # print "tmpl_param: " . Dumper($tmpl_param) . "\n";
        $tmpl->param($tmpl_param);
    }

    if ($archiver->contenttype_group_based) {
        require MT::Promise;
        my $contents = sub {
            $archiver->archive_group_contents($ctx);
        };
        $ctx->stash('contents', MT::Promise::delay($contents));
    }

    $ctx->stash('blog', $blog);
    $ctx->stash('_basename', fileparse($map->{__saved_output_file}, qr/\.[^.]*/));
    $ctx->stash('current_mapping_url', $url);

    if(!$map->is_preferred) {
        my $category = $ctx->{__stash}{archive_category};
        my $author = $ctx->{__stash}{author};
        $ctx->stash(
            'preferred_mapping_url',
            sub {
                my $file = $mt->archive_file_for(
                    $content_data, $blog, $at, $category, undef, $start, $author);
                my $url = $base_url . $file;
                $url =~ s{(?<!:)//+}{/}g;
                return $url;
            }
        );
    }

    my $html = undef;
    $html = $tmpl->build($ctx, $cond)
        or return $mt->error("Failed to build template: " . $tmpl->errstr);

    # Remove leading whitespace
    $html =~ s/\A(?:\s|\x{feff}|\xef\xbb\xbf)+(<(?:\?xml|!DOCTYPE))/$1/s;

    # Make arichive file name
    my $archive_file = File::Spec->catfile(
        $blog->archive_path || $blog->site_path,
        $content_data->archive_file
    );

    MT->run_callbacks('built_content', $mt, $html, $archive_file);

    return 1;
}

sub preview_cd_template {
    my $app = shift;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';

    my $perms = $app->permissions;
    my $org_permissions = $perms->permissions || '';
    my $permissions;
    if($perms->can_do('preview') && !$perms->can_do('edit_templates')) {
        $permissions = $org_permissions . ",'edit_templates'";
        $perms->permissions($permissions);
    }

    my $blog_id = $app->param('blog_id');
    MT->request('dynamic_preview_include_hold_entry',
        $plugin->dynamic_preview_include_hold_entry($blog_id));
    MT->request('dynamic_preview_include_future_entry',
        $plugin->dynamic_preview_include_future_entry($blog_id));

    my $id = $app->param('id');
    my $tmpl = $id ? MT->model('template')->load($id) : undef;
    if(!$tmpl) {
        return $app->errtrans('Invalid request: Template is required.');
    }

    my $map_id = $app->param('map_id');
    my $map = $map_id ? MT->model('templatemap')->load($map_id) : undef;
    if(!$map) {
        $map = MT->model('templatemap')->load({
            template_id => $tmpl->id,
            is_preferred => 1
        });
        if(!$map) {
            $map = MT->model('templatemap')->load({
                template_id => $tmpl->id,
            });
            if(!$map) {
                return $app->errtrans('Invalid request: Template map is required.');
            }
        }
    }
    my $at = $app->param('archive_type') || $map->archive_type;
    if(!$at) {
        return $app->errtrans('Invalid request: Archive type is required.');
    }

    my $cd_id = $app->param('cd_id');
    my $cd = $cd_id ? MT->model('content_data')->load($cd_id) : undef;
    if(!$cd) {
        $cd = MT->model('content_data')->load({
            blog_id => $blog_id,
            content_type_id => $tmpl->content_type_id,
        });
        if(!$cd) {
            return $app->errtrans('Invalid request: Saved content data is required.');
        }
    }

    # error if content_type_id is not same
    if($tmpl->content_type_id && $cd->content_type_id != $tmpl->content_type_id) {
        return $app->errtrans('The template [_1] is not associated with the content type [_2].', $tmpl->name, $cd->content_type->name);
    }

    my $cat_id = $app->param('cat_id');
    my $cat = $cat_id ? MT->model('category')->load($cat_id) : undef;
    if(!$cat && $map->cat_field) {
        my @cats = _get_cd_categories($cd);
        $cat = $cats[0];
        if(!$cat) {
            my $cat_field = $map->cat_field;
            if($cat_field && $cat_field->related_cat_set_id) {
                my $cat_set = MT->model('category_set')->load($cat_field->related_cat_set_id);
                if($cat_set) {
                    my $cats = $cat_set->categories;
                    $cat = $cats->[0];
                }
            }
            if(!$cat) {
                $app->log($plugin->translate('No category found for arichive template.'));
                return $app->errtrans('Invalid request: Saved category is required.');
            }
        }
    }

    my ($archive_html, $archive_file);
    my $callback = sub {
        my ($cb, $app, $html, $file) = @_;
        $archive_html = $html;
        $archive_file = $file;
    };

    my $cb = MT->add_callback('built_content', 5, $app, $callback);

    require MT::ContentPublisher;
    no strict 'refs';
    no warnings 'redefine';
    my $saved_rebuild_file = \&MT::ContentPublisher::rebuild_file;
    local *MT::ContentPublisher::rebuild_file = \&_rebuild_file;

    my $publisher = $app->publisher;
    my $res = $publisher->_rebuild_content_archive_type(
        ContentData => $cd,
        Blog => $cd->blog,
        Category => $cat,
        ArchiveType => $map->archive_type,
        TemplateMap => $map,
        Author => $cd->author,
    );

    MT->remove_callback($cb);

    if(!$res) {
        return $app->errtrans("Failed to build archive template: [_1]", $publisher->errstr);
    }

    if ($archive_html) {
        # Simulate the include_virtual
        $archive_html = $plugin->include_virtual($blog_id, $archive_html, $archive_file);
        # Insert base tag for relative urls
        $archive_html = $plugin->insert_base_tag($blog_id, $archive_html, $archive_file, $cd);
        # Replace URLs
        $archive_html = $plugin->replace_url($blog_id, $archive_html, $archive_file);
        # Replace string
        $archive_html = $plugin->replace_string($blog_id, $archive_html, $archive_file);
    }
    else {
        $app->log($plugin->translate('No output from _rebuild_content_archive_type.'));
    }

    return $archive_html;
}

sub create_role {
    my ($app) = @_;
    
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';
    if($plugin->has_extra) {
        return DynamicPreview::Extra::create_role(@_);
    }
    return $app->json_error('Not supported');
}

sub _create_temp_content_data {
    my $app         = shift;

    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';

    my $type        = $app->param('_type') || 'entry';
    my $entry_class = $app->model($type);
    my $blog_id     = $app->param('blog_id');
    my $blog        = $app->blog;
    my $id          = $app->param('id');
    my $content_type_id = $app->param('content_type_id');
    my $user_id = $app->user->id;
    my $label = $app->param('data_label');
    my $from_external_link = $app->param('from_external_link');

    return $app->errtrans('Invalid request.')
        unless $blog_id && $content_type_id;

    my $rn = $app->param('r');
    if(!$rn && $plugin->dynamic_preview_load_latest_revision) {
        my $rev = $plugin->latest_revision($type, $id);
        $rn = $rev->rev_number if $rev;
    }

    my $content_data;
    if ($id) {
        $content_data = MT::ContentData->load({
            id => $id,
            blog_id => $blog_id,
            content_type_id => $content_type_id,
        }) or return $app->errtrans("Invalid request.");
            
        if($rn && $blog->use_revision) {
            my $rev = $content_data->load_revision({ rev_number => $rn });
            if($rev && @$rev) {
                $content_data = $rev->[0];
            }
        }
        $user_id = $content_data->author_id;
    }
    else {
        $content_data = $entry_class->new;
        $content_data->set_values({
            id => -1,
            author_id => $user_id,
            blog_id => $blog_id,
            content_type_id => $content_type_id,
        });
    }

    return $app->return_to_dashboard( permission => 1 )
        unless $app->permissions->can_edit_content_data( $content_data, $app->user );

    if(!$from_external_link) {
        $content_data->status(scalar $app->param('status'));
        $content_data->label($label);
    
        my $content_field_types = $app->registry('content_field_types');
        my $content_type = $content_data->content_type;
        my $field_data = $content_type->fields;
        require MT::CMS::ContentData;
        my $data = {};
        foreach my $f (@$field_data) {
            my $content_field_type = $content_field_types->{$f->{type}};
            $data->{$f->{id}} = MT::CMS::ContentData::_get_form_data($app, $content_field_type, $f);
        }
        $content_data->data($data);
    }

    return $content_data;
}

sub preview_content_data {
    my $app = shift;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';

    $app->validate_magic or return;

    my $type = $app->param('_type') || 'content_data'; 
    my $perms = $app->permissions;
    my $org_permissions = $perms->permissions || '';
    my $permissions;
    if($perms->can_do('preview')) {
        $permissions = $org_permissions;
        $permissions .= ",'edit_all_content_data'";
        $perms->permissions($permissions);
    }

    require MT::CMS::ContentData;
    my $tmpl = eval {
        # _setup_category_ids_param($app);
        MT::CMS::ContentData::preview($app, @_);
    };
    if($@) {
        trace("preview_content_data: err: $@");
    }

    if($permissions) {
        $perms->permissions($org_permissions);
        $permissions = undef;
    }

    if(!$tmpl && $app->{redirect}) {
        $app->{redirect} = undef;
        return $app->errtrans('No permissions');
    }
    return $tmpl;
}

sub to_data_url {
    my ($app, $response) = @_;
    my $base64_content = encode_base64($response->content);
    my $mime_type = $response->content_type;
    my $data_url = sprintf('data:%s;base64,%s', $mime_type, $base64_content);
    return $data_url;
}

sub handle_raw_response {
    my ($app, $response, $inline) = @_;
    return if $app->{finalized}++;
    my $disposition = $inline ? 'inline' : 'attachment';
    $app->set_header('Content-Type', $response->content_type);
    $app->set_header('Content-Disposition', $disposition . '; filename="' . $response->filename . '"');
    $app->set_header('Content-Length', length($response->content));
    $app->send_http_header($response->content_type);
    $app->{no_print_body} = 1;
    my $chunk_size = 8192;
    my $content = $response->content;
    my $length = length($content);
    for (my $offset = 0; $offset < $length; $offset += $chunk_size) {
        my $chunk = substr($content, $offset, $chunk_size);
        $app->print($chunk);
    }
    return undef;
}

sub handle_raw_error {
    my ($app, $msg, $status, $url) = @_;
    return if $app->{finalized}++;
    $app->response_code($status);
    $app->response_message(status_message($status));
    $app->send_http_header('text/plain; charset=utf-8');
    $app->{no_print_body} = 1;
    return undef;
}

sub handle_response {
    my ($app, $response) = @_;
    return if $app->{finalized}++;
    $app->set_header('X-Content-Type-Options', 'nosniff');
    $app->send_http_header('application/json');
    $app->{no_print_body} = 1;
    eval {
        my $json = MT::Util::to_json({
            contents => to_data_url($app, $response),
            status => {
                url => $response->request->uri->as_string,
                content_type => $response->content_type,
                http_code => $response->code,
            }
        });
        $app->print_encode($json);
    };
    if($@) {
        $app->log('handle_response: error: ' . $@);
    }
    return undef;
}

sub handle_error {
    my ($app, $msg, $status, $url) = @_;
    return if $app->{finalized}++;
    $app->set_header('X-Content-Type-Options', 'nosniff');
    $app->send_http_header('application/json');
    $app->{no_print_body} = 1;
    $app->print_encode( MT::Util::to_json({
        contents => $msg,
        status => {
            url => $url || '',
            content_type => 'text/html; charset=utf-8',
            http_code => $status,
        }
    }) );
    return undef;
}

sub load_image {
    my $app = shift;
    my $plugin = $app->component('DynamicPreview')
        or die 'Plugin not found';
    my $src = $app->param('src') || $app->param('url') || '';
    unless($src) {
        return handle_raw_error($app, 'Missing parameter: src', 400);
    }
    my $blog_id = $app->param('blog_id');
    unless($blog_id) {
        return handle_raw_error($app, 'Missing parameter: blog_id', 400);
    }

    # extract hostname from $src
    my $src_uri = URI->new($src);
    unless($src_uri->scheme && $src_uri->scheme =~ /^https?$/) {
        return handle_raw_error($app, 'Invalid parameter: src, scheme', 400);
    }
    unless($src_uri->can('host')) {
        return handle_raw_error($app, 'Invalid parameter: src, host', 400);
    }
    my $hostname = $src_uri->host;

    # load basic auth for imageURL
    my $value = $plugin->dynamic_preview_basic_auth_for_image_url($blog_id) || '';
    my @lines = split /\r?\n/, $value;
    my %auth;
    foreach my $line (@lines) {
        $line =~ s!^\s*(.+)\s*$!$1!;
        my ($host, $user, $pass) = split /\s*,\s*/, $line;
        $auth{$host} = { user => $user, pass => $pass };
    }

    # get basic auth for hostname
    my $basic_auth = $auth{$hostname};

    my $ua = $app->new_ua({
        timeout => 30,
        max_size => undef,
    });
    unless($ua) {
        return handle_raw_error($app, 'Failed to create a new user agent.', 500);
    }
    my $req = HTTP::Request->new(GET => $src);
    if($basic_auth && $basic_auth->{user} && $basic_auth->{pass}) {
        $req->authorization_basic($basic_auth->{user}, $basic_auth->{pass});
    }
    my $res = $ua->request($req);
    if($res->is_success) {
        return handle_raw_response($app, $res);
    }
    else {
        $app->log(sprintf("Failed to load image: req: %s, status: %s, code: %d",
            $req->as_string, $res->status_line, $res->code));
        return handle_raw_error($app, 'Failed to load image: ' . $res->status_line, $res->code, $src);
    }
}

1;
__END__


