# Movable Type (r) (C) 2001-2010 Six Apart, Ltd. All Rights Reserved.
# This code cannot be redistributed without permission from www.sixapart.com.
# For more information, consult your Movable Type license.
#
# $Id: CMS.pm 4121 2009-07-28 23:08:19Z plim $

package MT::App::CMS;

use strict;
use base qw( MT::App );

use MT::Util qw( format_ts epoch2ts perl_sha1_digest_hex perl_sha1_digest
    remove_html );

sub LISTING_DATE_FORMAT ()      {'%b %e, %Y'}
sub LISTING_DATETIME_FORMAT ()  {'%b %e, %Y'}
sub LISTING_TIMESTAMP_FORMAT () {"%Y-%m-%d %I:%M:%S%p"}
sub NEW_PHASE ()                {1}

sub id {'cms'}

sub init {
    my $app = shift;
    $app->SUPER::init(@_) or return;
    $app->{state_params} = [
        '_type',      'id',         'tab',     'offset',
        'filter',     'filter_val', 'blog_id', 'is_power_edit',
        'filter_key', 'type'
    ];
    $app->{template_dir}         = 'cms';
    $app->{plugin_template_path} = '';
    $app->{is_admin}             = 1;
    $app->{default_mode}         = 'dashboard';
    $app;
}

sub core_methods {
    my $app = shift;
    my $pkg = '$Core::MT::CMS::';
    return {
        'tools'     => "${pkg}Tools::system_check",
        'dashboard' => "${pkg}Dashboard::dashboard",
        'menu'      => "${pkg}Dashboard::dashboard",
        'admin'     => "${pkg}Dashboard::dashboard",

        ## Generic handlers
        'save'           => "${pkg}Common::save",
        'edit'           => "${pkg}Common::edit",
        'view'           => "${pkg}Common::edit",
        'list'           => "${pkg}Common::list",
        'delete'         => "${pkg}Common::delete",
        'search_replace' => "${pkg}Search::search_replace",

        ## Edit methods
        'edit_role'   => "${pkg}User::edit_role",
        'edit_widget' => "${pkg}Template::edit_widget",

        ## Listing methods
        'list_ping'     => "${pkg}TrackBack::list",
        'list_entry'    => "${pkg}Entry::list",
        'list_template' => "${pkg}Template::list",
        'list_widget'   => "${pkg}Template::list_widget",
        'list_page'     => "${pkg}Page::list",
        'list_comment'  => "${pkg}Comment::list",
        'list_member'      => "${pkg}User::list_member",
        'list_user'        => "${pkg}User::list",
        'list_author'      => "${pkg}User::list",
        'list_commenter'   => "${pkg}Comment::list_commenter",
        'list_asset'       => "${pkg}Asset::list",
        'list_blog'        => "${pkg}Blog::list",
        'list_category'    => "${pkg}Category::list",
        'list_tag'         => "${pkg}Tag::list",
        'list_association' => "${pkg}User::list_association",
        'list_role'        => "${pkg}User::list_role",

        'asset_insert'        => "${pkg}Asset::insert",
        'asset_userpic'       => "${pkg}Asset::asset_userpic",
        'save_commenter_perm' => "${pkg}Comment::save_commenter_perm",
        'trust_commenter'     => "${pkg}Comment::trust_commenter",
        'ban_commenter'       => "${pkg}Comment::ban_commenter",
        'approve_item'        => "${pkg}Comment::approve_item",
        'unapprove_item'      => "${pkg}Comment::unapprove_item",
        'preview_entry'       => "${pkg}Entry::preview",

        ## Blog configuration screens
        'cfg_archives'     => "${pkg}Blog::cfg_archives",
        'cfg_prefs'        => "${pkg}Blog::cfg_prefs",
        'cfg_plugins'      => "${pkg}Plugin::cfg_plugins",
        'cfg_comments'     => "${pkg}Comment::cfg_comments",
        'cfg_trackbacks'   => "${pkg}TrackBack::cfg_trackbacks",
        'cfg_registration' => "${pkg}Comment::cfg_registration",
        'cfg_spam'         => "${pkg}Comment::cfg_spam",
        'cfg_entry'        => "${pkg}Entry::cfg_entry",
        'cfg_web_services' => "${pkg}Blog::cfg_web_services",

        ## Save
        'save_cat'     => "${pkg}Category::save",
        'save_entries' => "${pkg}Entry::save_entries",
        'save_pages'   => "${pkg}Page::save_pages",
        'save_entry'   => "${pkg}Entry::save",
        'save_role'    => "${pkg}User::save_role",
        'save_widget'  => "${pkg}Template::save_widget",

        ## Delete
        'delete_entry'  => "${pkg}Entry::delete",
        'delete_widget' => "${pkg}Template::delete_widget",

        ## List actions
        'enable_object'  => "${pkg}User::enable",
        'disable_object' => "${pkg}User::disable",
        'list_action'    => "${pkg}Tools::do_list_action",
        'empty_junk'     => "${pkg}Comment::empty_junk",
        'handle_junk'    => "${pkg}Comment::handle_junk",
        'not_junk'       => "${pkg}Comment::not_junk",

        'ping'               => "${pkg}Entry::send_pings",
        'rebuild_phase'      => "${pkg}Blog::rebuild_phase",
        'rebuild'            => "${pkg}Blog::rebuild_pages",
        'rebuild_new_phase'  => "${pkg}Blog::rebuild_new_phase",
        'start_rebuild'      => "${pkg}Blog::start_rebuild_pages",
        'rebuild_confirm'    => "${pkg}Blog::rebuild_confirm",
        'entry_notify'       => "${pkg}AddressBook::entry_notify",
        'send_notify'        => "${pkg}AddressBook::send_notify",
        'start_upload'       => "${pkg}Asset::start_upload",
        'upload_file'        => "${pkg}Asset::upload_file",
        'upload_userpic'     => "${pkg}User::upload_userpic",
        'complete_insert'    => "${pkg}Asset::complete_insert",
        'complete_upload'    => "${pkg}Asset::complete_upload",
        'start_upload_entry' => "${pkg}Asset::start_upload_entry",
        'logout'             => {
            code           => sub { $_[0]->SUPER::logout(@_) },
            requires_login => 0,
        },
        'start_recover' => {
            code           => "${pkg}Tools::start_recover",
            requires_login => 0,
        },
        'recover' => {
            code           => "${pkg}Tools::recover_password",
            requires_login => 0,
        },
        'new_pw' => {
            code => "${pkg}Tools::new_password",
            requires_login => 0,
        },

        'view_rpt_log'         => "${pkg}RptLog::view",
        'view_log'             => "${pkg}Log::view",
        'list_log'             => "${pkg}Log::view",
        'reset_rpt_log'        => "${pkg}RptLog::reset",
        'reset_log'            => "${pkg}Log::reset",
        'export_log'           => "${pkg}Log::export",
        'export_notification'  => "${pkg}AddressBook::export",
        'start_import'         => "${pkg}Import::start_import",
        'start_export'         => "${pkg}Export::start_export",
        'export'               => "${pkg}Export::export",
        'import'               => "${pkg}Import::do_import",
        'pinged_urls'          => "${pkg}Entry::pinged_urls",
        'save_entry_prefs'     => "${pkg}Entry::save_entry_prefs",
        'save_favorite_blogs'  => "${pkg}Blog::save_favorite_blogs",
        'folder_add'           => "${pkg}Category::category_add",
        'category_add'         => "${pkg}Category::category_add",
        'category_do_add'      => "${pkg}Category::category_do_add",
        'cc_return'            => "${pkg}Blog::cc_return",
        'reset_blog_templates' => "${pkg}Template::reset_blog_templates",
        'handshake'            => "${pkg}Blog::handshake",
        'itemset_action'       => "${pkg}Tools::do_list_action",
        'page_action'          => "${pkg}Tools::do_page_action",
        'cfg_system'           => "${pkg}Tools::cfg_system_general",
        'cfg_system_users'     => "${pkg}User::cfg_system_users",
        'cfg_system_feedback'  => "${pkg}Comment::cfg_system_feedback",
        'save_plugin_config'   => "${pkg}Plugin::save_config",
        'reset_plugin_config'  => "${pkg}Plugin::reset_config",
        'save_cfg_system_feedback' =>
            "${pkg}Comment::save_cfg_system_feedback",
        'save_cfg_system_general' => "${pkg}Tools::save_cfg_system_general",
        'save_cfg_system_users'   => "${pkg}User::save_cfg_system_users",
        'update_welcome_message'  => "${pkg}Blog::update_welcome_message",
        'upgrade'                 => {
            code           => "${pkg}Tools::upgrade",
            requires_login => 0,
        },
        'plugin_control'           => "${pkg}Plugin::plugin_control",
        'recover_profile_password' => "${pkg}User::recover_profile_password",
        'rename_tag'               => "${pkg}Tag::rename_tag",
        'remove_user_assoc'        => "${pkg}User::remove_user_assoc",
        'revoke_role'              => "${pkg}User::revoke_role",
        'grant_role'               => "${pkg}User::grant_role",
        'start_backup'             => "${pkg}Tools::start_backup",
        'start_restore'            => "${pkg}Tools::start_restore",
        'backup'                   => "${pkg}Tools::backup",
        'backup_download'          => "${pkg}Tools::backup_download",
        'restore'                  => "${pkg}Tools::restore",
        'restore_premature_cancel' => "${pkg}Tools::restore_premature_cancel",
        'adjust_sitepath'          => "${pkg}Tools::adjust_sitepath",
        'system_check'             => "${pkg}Tools::system_check",
        'dialog_refresh_templates' =>
            "${pkg}Template::dialog_refresh_templates",
        'dialog_clone_blog' => "${pkg}Common::clone_blog",
                'dialog_publishing_profile' =>
            "${pkg}Template::dialog_publishing_profile",
        'refresh_all_templates' => "${pkg}Template::refresh_all_templates",
        'preview_template'      => "${pkg}Template::preview",
        'publish_index_templates' =>
            "${pkg}Template::publish_index_templates",
        'publish_archive_templates' =>
            "${pkg}Template::publish_archive_templates",
        'publish_templates_from_search' =>
            "${pkg}Template::publish_templates_from_search",

        ## Comment Replies
        reply         => "${pkg}Comment::reply",
        do_reply      => "${pkg}Comment::do_reply",
        reply_preview => "${pkg}Comment::reply_preview",

        ## Dialogs
        'dialog_restore_upload'  => "${pkg}Tools::dialog_restore_upload",
        'dialog_adjust_sitepath' => "${pkg}Tools::dialog_adjust_sitepath",
        'dialog_post_comment'    => "${pkg}Comment::dialog_post_comment",
        'dialog_select_weblog'   => "${pkg}Blog::dialog_select_weblog",
        'dialog_select_sysadmin' => "${pkg}User::dialog_select_sysadmin",
        'dialog_grant_role'      => "${pkg}User::dialog_grant_role",
        'dialog_select_author'   => "${pkg}User::dialog_select_author",

        ## AJAX handlers
        'delete_map'        => "${pkg}Template::delete_map",
        'add_map'           => "${pkg}Template::add_map",
        'js_tag_check'      => "${pkg}Tag::js_tag_check",
        'js_tag_list'       => "${pkg}Tag::js_tag_list",
        'convert_to_html'   => "${pkg}Tools::convert_to_html",
        'update_list_prefs' => "${pkg}Tools::update_list_prefs",
        'js_add_category'   => "${pkg}Category::js_add_category",
        'remove_userpic'    => "${pkg}User::remove_userpic",

        # declared in MT::App
        'update_widget_prefs' =>
            sub { return shift->update_widget_prefs(@_) },

        'js_recent_entries_for_tag' => "${pkg}Tag::js_recent_entries_for_tag",

        ## DEPRECATED ##
        'list_pings'    => "${pkg}TrackBack::list",
        'list_entries'  => "${pkg}Entry::list",
        'list_pages'    => "${pkg}Page::list",
        'list_comments' => "${pkg}Comment::list",
        'list_authors'      => "${pkg}User::list",
        'list_assets'       => "${pkg}Asset::list",
        'list_cat'          => "${pkg}Category::list",
        'list_blogs'        => "${pkg}Blog::list",
        'list_associations' => "${pkg}User::list_association",
        'list_roles'        => "${pkg}User::list_role",
    };
}

sub core_widgets {
    my $app = shift;
    my $pkg = '$Core::MT::CMS::';
    return {
        new_install => {
            template => 'widget/new_install.tmpl',
            set      => 'main',    # forces this widget to the main group
            singular => 1,
        },
        new_user => {
            template => 'widget/new_user.tmpl',
            set      => 'main',    # forces this widget to the main group
            singular => 1,
        },
        new_version => {
            template => 'widget/new_version.tmpl',
            set      => 'main',
            singular => 1,
            handler  => "${pkg}Dashboard::new_version_widget",
        },
        this_is_you => {
            label    => 'This is You',
            template => 'widget/this_is_you.tmpl',
            handler  => "${pkg}Dashboard::this_is_you_widget",
            set      => 'sidebar',
            singular => 1,
        },
        mt_shortcuts => {
            label    => 'Handy Shortcuts',
            template => 'widget/mt_shortcuts.tmpl',
            singular => 1,
            set      => 'sidebar',
        },
        mt_news => {
            label    => 'Movable Type News',
            template => 'widget/mt_news.tmpl',
            handler  => "${pkg}Dashboard::mt_news_widget",
            singular => 1,
            set      => 'sidebar',
        },
        blog_stats => {
            label    => 'Blog Stats',
            template => 'widget/blog_stats.tmpl',
            handler  => "${pkg}Dashboard::mt_blog_stats_widget",
            singular => 1,
            set      => 'main',
        },
    };
}

sub core_blog_stats_tabs {
    my $app = shift;
    my $pkg = '$Core::MT::CMS::';
    return {
        entry => {
            label    => 'Entries',
            template => 'widget/blog_stats_entry.tmpl',
            handler  => "${pkg}Dashboard::mt_blog_stats_widget_entry_tab",
            stats    => "${pkg}Dashboard::generate_dashboard_stats_entry_tab",
        },
        comment => {
            label    => 'Comments',
            template => 'widget/blog_stats_comment.tmpl',
            handler  => "${pkg}Dashboard::mt_blog_stats_widget_comment_tab",
            stats => "${pkg}Dashboard::generate_dashboard_stats_comment_tab",
        },
        tag_cloud => {
            label    => 'Tag Cloud',
            handler  => "${pkg}Dashboard::mt_blog_stats_tag_cloud_tab",
            template => 'widget/blog_stats_tag_cloud.tmpl',
        },
    };
}

sub core_page_actions {
    return {
        list_templates => {
            refresh_all_blog_templates => {
                label     => "Refresh Blog Templates",
                dialog    => 'dialog_refresh_templates',
                condition => sub {
                    MT->app->blog,;
                },
                order => 1000,
            },
            refresh_global_templates => {
                label     => "Refresh Global Templates",
                dialog    => 'dialog_refresh_templates',
                condition => sub {
                    !MT->app->blog,;
                },
                order => 1000,
            },
            publishing_profile => {
                label     => "Use Publishing Profile",
                dialog    => 'dialog_publishing_profile',
                condition => sub {
                    MT->app->blog,;
                },
                order => 1100,
            },
        },
    };
}

sub init_plugins {
    my $app = shift;

    # This has to be done prior to plugin initialization since we
    # may have plugins that register themselves using some of the
    # older callback names. The callback aliases are declared
    # in init_core_callbacks.
    $app->init_core_callbacks();
    $app->SUPER::init_plugins(@_);
}

sub init_request {
    my $app = shift;
    $app->SUPER::init_request(@_);
    $app->set_no_cache;
    $app->{requires_login} = 1
        unless exists $app->{requires_login};   # by default, we require login

    my $mode = $app->mode;

    # Global 'blog_id' parameter check; if we get something
    # other than an integer, die
    if ( my $blog_id = $app->param('blog_id') ) {
        if ( $blog_id ne int($blog_id) ) {
            die $app->translate("Invalid request");
        }
    }

    unless ( defined $app->{upgrade_required} ) {
        $app->{upgrade_required} = 0;
        if (   ( $mode ne 'logout' )
            && ( $mode ne 'start_recover' )
            && ( $mode ne 'recover' )
            && ( $mode ne 'upgrade' ) )
        {
            my $schema  = $app->config('SchemaVersion');
            my $version = $app->config('MTVersion');
            if (   !$schema
                || ( $schema < $app->schema_version )
                || ( ( !$version || ( $version < $app->version_number ) )
                    && $app->config->NotifyUpgrade )
                )
            {
                $app->{upgrade_required} = 1;
            }
            else {
                foreach my $plugin (@MT::Components) {
                    if ( $plugin->needs_upgrade ) {
                        $app->{upgrade_required} = 1;
                        last;
                    }
                }
            }
        }
    }

    if ( $app->{upgrade_required} ) {
        $app->{requires_login} = 0;
        $app->mode('upgrade');
    }
}

sub core_list_actions {
    my $app = shift;
    my $pkg = '$Core::MT::CMS::';
    return {
        'entry' => {
            'set_draft' => {
                label      => "Unpublish Entries",
                order      => 200,
                code       => "${pkg}Entry::draft_entries",
                permission => 'edit_all_posts,publish_post',
                condition  => sub {
                    return 0 if $app->mode eq 'view';
                    return $app->blog && $app->blog->site_path ? 1 : 0;
                    }
            },
            'add_tags' => {
                label       => "Add Tags...",
                order       => 300,
                code        => "${pkg}Tag::add_tags_to_entries",
                input       => 1,
                input_label => 'Tags to add to selected entries',
                permission  => 'edit_all_posts',
                condition   => sub {
                    return $app->mode ne 'view';
                    }
            },
            'remove_tags' => {
                label       => "Remove Tags...",
                order       => 400,
                code        => "${pkg}Tag::remove_tags_from_entries",
                input       => 1,
                input_label => 'Tags to remove from selected entries',
                permission  => 'edit_all_posts',
                condition   => sub {
                    return $app->mode ne 'view';
                    }
            },
            'open_batch_editor' => {
                label     => "Batch Edit Entries",
                code      => "${pkg}Entry::open_batch_editor",
                order     => 500,
                condition => sub {
                    return 0 if $app->mode eq 'view';
                    $app->param('blog_id')
                        && ( $app->user->is_superuser()
                            || $app->permissions->can_edit_all_posts )
                        && $app->param('filter_val') != MT::Entry::JUNK()
                        && $app->param('filter_key') ne 'spam_entries'
                },
            },
        },
        'page' => {
            'set_draft' => {
                label      => "Unpublish Pages",
                order      => 200,
                code       => "${pkg}Entry::draft_entries",
                permission => 'manage_pages',
                condition  => sub {
                    return 0 if $app->mode eq 'view';
                    return $app->blog && $app->blog->site_path ? 1 : 0;
                },
            },
            'add_tags' => {
                label       => "Add Tags...",
                order       => 300,
                code        => "${pkg}Tag::add_tags_to_entries",
                input       => 1,
                input_label => 'Tags to add to selected pages',
                permission  => 'manage_pages',
                condition   => sub {
                    return $app->mode ne 'view';
                },
            },
            'remove_tags' => {
                label       => "Remove Tags...",
                order       => 400,
                code        => "${pkg}Tag::remove_tags_from_entries",
                input       => 1,
                input_label => 'Tags to remove from selected pages',
                permission  => 'manage_pages',
                condition   => sub {
                    return $app->mode ne 'view';
                },
            },
            'open_batch_editor' => {
                label     => "Batch Edit Pages",
                code      => "${pkg}Entry::open_batch_editor",
                order     => 500,
                condition => sub {
                    return 0 if $app->mode eq 'view';
                    $app->param('blog_id')
                        && ( $app->user->is_superuser()
                        || $app->permissions->can_manage_pages );
                },
            },
        },
        'asset' => {
            'add_tags' => {
                label       => "Add Tags...",
                order       => 100,
                code        => "${pkg}Tag::add_tags_to_assets",
                input       => 1,
                input_label => 'Tags to add to selected assets',
                permission  => 'edit_assets',
            },
            'remove_tags' => {
                label       => "Remove Tags...",
                order       => 200,
                code        => "${pkg}Tag::remove_tags_from_assets",
                input       => 1,
                input_label => 'Tags to remove from selected assets',
                permission  => 'edit_assets',
            },
        },
        'ping' => {
            'unapprove_ping' => {
                label      => "Unpublish TrackBack(s)",
                order      => 100,
                code       => "${pkg}Comment::unapprove_item",
                permission => 'edit_all_posts,manage_feedback,publish_post',
            },
        },
        'comment' => {
            'unapprove_comment' => {
                label      => "Unpublish Comment(s)",
                order      => 100,
                code       => "${pkg}Comment::unapprove_item",
                permission => 'edit_all_posts,manage_feedback,publish_post',
                condition  => sub {
                    return 1;
                },
            },
            'trust_commenter' => {
                label      => "Trust Commenter(s)",
                order      => 200,
                code       => "${pkg}Comment::trust_commenter_by_comment",
                permission => 'manage_feedback',
            },
            'untrust_commenter' => {
                label      => "Untrust Commenter(s)",
                order      => 300,
                code       => "${pkg}Comment::untrust_commenter_by_comment",
                permission => 'manage_feedback',
            },
            'ban_commenter' => {
                label      => "Ban Commenter(s)",
                order      => 400,
                code       => "${pkg}Comment::ban_commenter_by_comment",
                permission => 'manage_feedback',
            },
            'unban_commenter' => {
                label      => "Unban Commenter(s)",
                order      => 500,
                code       => "${pkg}Comment::unban_commenter_by_comment",
                permission => 'manage_feedback',
            },
        },
        'commenter' => {
            'untrust' => {
                label      => "Untrust Commenter(s)",
                order      => 100,
                code       => "${pkg}Comment::untrust_commenter",
                permission => 'manage_feedback',
            },
            'unban' => {
                label      => "Unban Commenter(s)",
                order      => 200,
                code       => "${pkg}Comment::unban_commenter",
                permission => 'manage_feedback',
            },
        },
        'author' => {
            'recover_passwords' => {
                label                   => "Recover Password(s)",
                order                   => 100,
                continue_prompt_handler => sub {
                    MT->translate("_WARNING_PASSWORD_RESET_MULTI");
                },
                code      => "${pkg}Tools::recover_passwords",
                condition => sub {
                    ( $app->user->is_superuser()
                            && MT::Auth->can_recover_password );
                },
            },
            'delete_user' => {
                label                   => "Delete",
                order                   => 200,
                continue_prompt_handler => sub {
                    $app->config->ExternalUserManagement
                        ? MT->translate("_WARNING_DELETE_USER_EUM")
                        : MT->translate("_WARNING_DELETE_USER");
                },
                code      => "${pkg}Common::delete",
                condition => sub {
                    $app->user->is_superuser();
                },
            },
        },
        'blog' => {
            refresh_blog_templates => {
                label                   => "Refresh Template(s)",
                continue_prompt_handler => sub {
                    MT->translate("_WARNING_REFRESH_TEMPLATES_FOR_BLOGS");
                },
                code => sub {
                    my $app = MT->app;
                    $app->param( 'backup', 1 );
                    require MT::CMS::Template;
                    MT::CMS::Template::refresh_all_templates( $app, @_ );
                },
            },
            clone_blog => {
                label => "Clone Blog",
                code => "${pkg}Common::clone_blog",
                permission => 'administer',
                max => 1,
                dialog => 1,
            },
        },
        'template' => {
            refresh_tmpl_templates => {
                label      => "Refresh Template(s)",
                code       => "${pkg}Template::refresh_individual_templates",
                permission => 'edit_templates',
                order      => 100,
            },

            # Now a button!
            # publish_index_templates => {
            #     label => "Publish Template(s)",
            #     code => "${pkg}Template::publish_index_templates",
            #     permission => 'rebuild',
            #     condition => sub {
            #         my $app = MT->app;
            #         my $tmpl_type = $app->param('filter_key');
            #         return $app->mode eq 'itemset_action'  ? 1
            #              : !$app->blog                     ? 0
            #              : !$tmpl_type                     ? 0
            #              : $tmpl_type eq 'index_templates' ? 1
            #              :                                   0
            #              ;
            #     },
            #     order => 200,
            # },
            # Now a button!
            # publish_archive_templates => {
            #     label      => "Publish Template(s)",
            #     code       => "${pkg}Template::publish_archive_templates",
            #     permission => 'rebuild',
            #     condition  => sub {
            #         my $app       = MT->app;
            #         my $tmpl_type = $app->param('filter_key');
            #         return $app->mode eq 'itemset_action' ? 1
            #           : !$app->blog ? 0
            #           : !$tmpl_type ? 0
            #           : $tmpl_type eq 'archive_templates' ? 1
            #           :                                     0;
            #     },
            #     order => 300,
            # },
            copy_templates => {
                label      => "Clone Template(s)",
                code       => "${pkg}Template::clone_templates",
                permission => 'edit_templates',
                condition  => sub {
                    my $app = MT->app;
                    my $tmpl_type = $app->param('filter_key') || '';
                    return
                          $tmpl_type eq 'system_templates' ? 0
                        : $tmpl_type eq 'email_templates'  ? 0
                        :                                    1;
                },
                order => 400,
            },
        },
    };
}

sub _entry_label {
    my $app = MT->instance;
    my $type = $app->param('type') || 'entry';
    $app->model($type)->class_label_plural;
}

sub core_list_filters {
    my $app = shift;
    return {
        asset => sub {
            require MT::CMS::Asset;
            return MT::CMS::Asset::asset_list_filters( $app, @_ );
        },
        entry => {
            published => {
                label => sub {
                    $app->translate( 'Published [_1]', _entry_label );
                },
                order   => 100,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    $terms->{status} = 2;
                },
            },
            unpublished => {
                label => sub {
                    $app->translate( 'Unpublished [_1]', _entry_label );
                },
                order   => 200,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    $terms->{status} = 1;
                },
            },
            scheduled => {
                label => sub {
                    $app->translate( 'Scheduled [_1]', _entry_label );
                },
                order   => 300,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    $terms->{status} = 4;
                },
            },
            my_posts => {
                label => sub {
                    $app->translate( 'My [_1]', _entry_label );
                },
                order   => 400,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    $terms->{author_id} = $app->user->id;
                },
            },
            received_comments_in_last_7_days => {
                label => sub {
                    $app->translate( '[_1] with comments in the last 7 days',
                        _entry_label );
                },
                order   => 500,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    my $ts = time - 7 * 24 * 60 * 60;
                    $ts = epoch2ts( MT->app->blog, $ts );
                    $args->{join} = MT::Comment->join_on(
                        'entry_id',
                        {   created_on  => [ $ts, undef ],
                            junk_status => MT::Comment::NOT_JUNK(),
                        },
                        { 
                            range_incl => { created_on => 1 }, 
                            unique => 1, 
                            sort => 'created_on', 
                            direction => 'descend' 
                        }
                    );
                },
            },
            _by_date => {
                label => sub {
                    my $app = MT->instance;
                    my $val = $app->param('filter_val');
                    my ( $from, $to ) = split /-/, $val;
                    $from = undef unless $from =~ m/^\d{8}$/;
                    $to   = undef unless $to   =~ m/^\d{8}$/;
                    my $format = '%x';
                    $from = format_ts(
                        $format, $from . '000000',
                        undef,   MT->current_language
                    ) if $from;
                    $to = format_ts(
                        $format, $to . '000000',
                        undef,   MT->current_language
                    ) if $to;
                    my $label = _entry_label;

                    if ( $from && $to ) {
                        return $app->translate(
                            '[_1] posted between [_2] and [_3]',
                            $label, $from, $to );
                    }
                    elsif ($from) {
                        return $app->translate( "[_1] posted since [_2]",
                            $label, $from );
                    }
                    elsif ($to) {
                        return $app->translate(
                            "[_1] posted on or before [_2]",
                            $label, $to );
                    }
                },
                handler => sub {
                    my ( $terms, $args ) = @_;
                    my $val = $app->param('filter_val');
                    my ( $from, $to ) = split /-/, $val;
                    $from = undef unless $from =~ m/^\d{8}$/;
                    $from .= '000000';
                    $to = undef unless $to =~ m/^\d{8}$/;
                    $to .= '235959';
                    $terms->{authored_on} = [ $from, $to ];
                    $args->{range_incl}{authored_on} = 1;
                },
            },
        },
        ping => {
            default => {
                label   => 'Non-spam TrackBacks',
                order   => 100,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::TBPing;
                    $terms->{junk_status} = MT::TBPing::NOT_JUNK();
                },
            },
            my_posts => {
                label   => 'TrackBacks on my entries',
                order   => 200,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::Entry;
                    my $app = MT->instance;
                    require MT::TBPing;
                    require MT::Trackback;
                    $terms->{junk_status} = MT::TBPing::NOT_JUNK();
                    $args->{join}         = MT::Trackback->join_on(
                        undef,
                        { id => \'= tbping_tb_id', },
                        {   join => MT::Entry->join_on(
                                undef,
                                {   id        => \'= trackback_entry_id',
                                    author_id => $app->user->id
                                }
                            )
                        },
                    );
                },
            },
            published => {
                label   => 'Published TrackBacks',
                order   => 200,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    $terms->{visible} = 1;
                },
            },
            unpublished => {
                label   => 'Unpublished TrackBacks',
                order   => 300,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::TBPing;
                    $terms->{junk_status} = MT::TBPing::NOT_JUNK();
                    $terms->{visible}     = 0;
                },
            },
            spam => {
                label   => 'TrackBacks marked as Spam',
                order   => 400,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::TBPing;
                    $terms->{junk_status} = MT::TBPing::JUNK();
                },
            },
            last_7_days => {
                label   => 'All TrackBacks in the last 7 days',
                order   => 700,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    my $ts = time - 7 * 24 * 60 * 60;
                    $ts = epoch2ts( MT->app->blog, $ts );
                    $terms->{created_on} = [ $ts, undef ];
                    $args->{range_incl}{created_on} = 1;
                    $terms->{junk_status} = MT::TBPing::NOT_JUNK();
                },
            },
        },
        comment => {
            default => {
                label   => 'Non-spam Comments',
                order   => 100,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::Comment;
                    $terms->{junk_status} = MT::Comment::NOT_JUNK();
                },
            },
            my_posts => {
                label   => 'Comments on my entries',
                order   => 200,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::Entry;
                    require MT::Comment;
                    my $app = MT->instance;
                    $terms->{junk_status} = MT::Comment::NOT_JUNK();

                    # This join syntax employs a hack that allows us
                    # to do joins on abitrary columns. Typically,
                    # objectdriver joins are applied with the primary
                    # key column of the driving table (here,
                    # mt_comment.comment_id), but we actually want to
                    # join to the mt_entry table where the entry_id
                    # matches to mt_comment.comment_entry_id (not the
                    # primary key). So by specifying NO 'join' column
                    # (which is always compared with the primary key),
                    # we specify the actual join conditions in the
                    # terms. And using a reference for the
                    # 'comment_entry_id' column name, to pass that
                    # on directly to the SQL statement that is generated.
                    $args->{join} = MT::Entry->join_on(
                        undef,
                        {   id        => \'= comment_entry_id',
                            author_id => $app->user->id
                        }
                    );
                },
            },
            unpublished => {
                label   => 'Pending comments',
                order   => 300,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::Comment;
                    $terms->{junk_status} = MT::Comment::NOT_JUNK();
                    $terms->{visible}     = 0;
                },
            },
            spam => {
                label   => 'Spam Comments',
                order   => 400,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::Comment;
                    $terms->{junk_status} = MT::Comment::JUNK();
                },
            },
            published => {
                label   => 'Published comments',
                order   => 500,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    $terms->{visible} = 1;
                },
            },
            last_7_days => {
                label   => 'Comments in the last 7 days',
                order   => 700,
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::Comment;
                    my $ts = time - 7 * 24 * 60 * 60;
                    $ts = epoch2ts( MT->app->blog, $ts );
                    $terms->{created_on} = [ $ts, undef ];
                    $args->{range_incl}{created_on} = 1;
                    $terms->{junk_status} = MT::Comment::NOT_JUNK();
                },
            },
            _comments_by_user => {
                label => sub {
                    my $app     = MT->app;
                    my $user_id = $app->param('filter_val');
                    my $user    = MT::Author->load($user_id);
                    require MT::Author;
                    return $app->translate(
                        "All comments by [_1] '[_2]'",
                        (     $user->type == MT::Author::COMMENTER()
                            ? $app->translate("Commenter")
                            : $app->translate("Author")
                        ),
                        (     $user->nickname
                            ? $user->nickname . ' (' . $user->name . ')'
                            : $user->name
                        )
                    );
                },
                handler => sub {
                    my ( $terms, $args ) = @_;
                    my $cmtr_id = int( MT->app->param('filter_val') );
                    $terms->{commenter_id} = $cmtr_id;
                },
            },
            _comments_by_entry => {
                label => sub {
                    my $entry_id = MT->app->param('filter_val');
                    my $entry    = MT::Entry->load($entry_id);
                    return MT->translate( "All comments for [_1] '[_2]'",
                        $entry->class_label, $entry->title );
                },
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::Comment;
                    my $entry_id = int( MT->app->param('filter_val') );
                    $terms->{entry_id}    = $entry_id;
                    $terms->{junk_status} = MT::Comment::NOT_JUNK();
                },
            },
            _by_date => {
                label => sub {
                    my $app = MT->instance;
                    my $val = $app->param('filter_val');
                    my ( $from, $to ) = split /-/, $val;
                    $from = undef unless $from =~ m/^\d{8}$/;
                    $to   = undef unless $to   =~ m/^\d{8}$/;
                    my $format = '%x';
                    $from = format_ts(
                        $format, $from . '000000',
                        undef,   MT->current_language
                    ) if $from;
                    $to = format_ts(
                        $format, $to . '000000',
                        undef,   MT->current_language
                    ) if $to;

                    if ( $from && $to ) {
                        return $app->translate(
                            'Comments posted between [_1] and [_2]',
                            $from, $to );
                    }
                    elsif ($from) {
                        return $app->translate( "Comments posted since [_1]",
                            $from );
                    }
                    elsif ($to) {
                        return $app->translate(
                            "Comments posted on or before [_1]", $to );
                    }
                },
                handler => sub {
                    my ( $terms, $args ) = @_;
                    require MT::Comment;
                    my $val = $app->param('filter_val');
                    my ( $from, $to ) = split /-/, $val;
                    $from = undef unless $from =~ m/^\d{8}$/;
                    $from .= '000000';
                    $to = undef unless $to =~ m/^\d{8}$/;
                    $to .= '235959';
                    $terms->{junk_status}           = MT::Comment::NOT_JUNK();
                    $terms->{created_on}            = [ $from, $to ];
                    $args->{range_incl}{created_on} = 1;
                },
            },
        },
        tag => {
            entry => {
                label => 'Tags with entries',
                order => 100,
            },
            page => {
                label => 'Tags with pages',
                order => 200,
            },
            asset => {
                label => 'Tags with assets',
                order => 300,
            },
        },
        sys_user => {
            enabled => {
                label   => 'Enabled Users',
                order   => 100,
                handler => sub {
                    my ($terms) = @_;
                    $terms->{status} = 1;
                },
            },
            disabled => {
                label   => "Disabled Users",
                order   => 200,
                handler => sub {
                    my ($terms) = @_;
                    $terms->{status} = 2;
                },
            },
            pending => {
                label   => "Pending Users",
                order   => 300,
                handler => sub {
                    my ($terms) = @_;
                    $terms->{status} = 3;
                },
            },
        },
        user => {
            author => {
                label   => 'Authors',
                order   => 100,
                handler => sub {
                    my ($terms) = @_;
                    require MT::Author;
                    $terms->{type} = MT::Author::AUTHOR();
                },
            },
            commenter => {
                label   => 'Commenters',
                order   => 200,
                handler => sub {
                    my ($terms) = @_;
                    require MT::Author;
                    $terms->{type} = MT::Author::COMMENTER();
                },
            },
        },
    };
}

sub core_menus {
    my $app = shift;
    return {
        'create' => {
            label => "Create",
            order => 100,
        },
        'manage' => {
            label => "Manage",
            order => 200,
        },
        'design' => {
            label => "Design",
            order => 300,
        },
        'prefs' => {
            label => "Preferences",
            order => 400,
        },
        'tools' => {
            label => "Tools",
            order => 500,
        },

        'create:blog' => {
            label => "Blog",
            order => 100,
            view  => "system",
        },
        'create:blog' => {
            label      => "Blog",
            order      => 200,
            view       => "system",
            mode       => "view",
            args       => { _type => "blog" },
            permission => "create_blog",
        },
        'create:user' => {
            label      => "User",
            order      => 200,
            view       => "system",
            mode       => "view",
            args       => { _type => "author" },
            permission => "administer",
            condition  => sub {
                return !MT->config->ExternalUserManagement;
            },
        },
        'create:entry' => {
            label      => "Entry",
            order      => 100,
            mode       => 'view',
            args       => { _type => 'entry' },
            permission => 'create_post',
            view       => "blog",
        },
        'create:page' => {
            label      => "Page",
            order      => 200,
            mode       => 'view',
            args       => { _type => 'page' },
            permission => 'manage_pages',
            view       => "blog",
        },
        'create:file' => {
            label      => "Upload File",
            order      => 300,
            dialog     => 'start_upload',
            permission => 'upload,edit_assets',
            view       => "blog",
        },

        'manage:blog' => {
            label => "Blogs",
            mode  => "list_blog",
            order => 100,
            view  => "system",
        },
        'manage:user' => {
            label      => "Users",
            mode       => "list_user",
            order      => 200,
            permission => "administer",
            view       => "system",
        },
        'manage:entry' => {
            label     => "Entries",
            mode      => 'list_entry',
            order     => 1000,
            condition => sub {
                return 1 if $app->user->is_superuser;
                if ( $app->param('blog_id') ) {
                    my $perms
                        = $app->user->permissions( $app->param('blog_id') );
                    return 1
                        if $perms->can_create_post
                            || $perms->can_publish_post
                            || $perms->can_edit_all_posts;
                }
                else {
                    require MT::Permission;
                    my @blogs
                        = map { $_->blog_id }
                        grep {
                        $_->can_create_post
                            || $_->can_publish_post
                            || $_->can_edit_all_posts
                        } MT::Permission->load(
                        { author_id => $app->user->id } );
                    return 1 if @blogs;
                }
                return 0;
            },

            #permission => 'create_post,publish_post,edit_all_posts',
        },
        'manage:comment' => {
            label     => "Comments",
            mode      => 'list_comment',
            order     => 2000,
            condition => sub {
                return 1 if $app->user->is_superuser;
                if ( $app->param('blog_id') ) {
                    my $perms
                        = $app->user->permissions( $app->param('blog_id') );
                    return 1
                        if $perms && $perms->can_view_feedback;
                }
                else {
                    require MT::Permission;
                    my @blogs
                        = map { $_->blog_id }
                        grep {
                            $_->can_view_feedback
                        } MT::Permission->load(
                        { author_id => $app->user->id } );
                    return 1 if @blogs;
                }
                return 0;
            },

          #permission => 'create_post,edit_all_posts,manage_feedback,comment',
        },
        'manage:asset' => {
            label      => "Assets",
            mode       => 'list_asset',
            order      => 3000,
            permission => 'edit_assets',
        },
        'manage:page' => {
            label      => "Pages",
            mode       => 'list_pages',
            order      => 4000,
            permission => 'manage_pages',
        },
        'manage:ping' => {
            label      => "TrackBacks",
            mode       => 'list_pings',
            order      => 5000,
            permission => 'create_post,edit_all_posts,manage_feedback',
        },
        'manage:category' => {
            label      => "Categories",
            mode       => 'list_cat',
            order      => 6000,
            permission => 'edit_categories',
            view       => "blog",
        },
        'manage:folder' => {
            label      => "Folders",
            mode       => 'list_cat',
            args       => { _type => 'folder' },
            order      => 7000,
            permission => 'manage_pages',
            view       => "blog",
        },
        'manage:tag' => {
            label             => "Tags",
            mode              => 'list_tag',
            order             => 8000,
            permission        => 'edit_tags',
            system_permission => 'administer',
        },
        'manage:blog_user' => {
            label             => "Users",
            mode              => 'list_member',
            order             => 9000,
            view              => "blog",
            permission        => 'administer_blog,manage_users',
            system_permission => 'administer',
        },
        'manage:notification' => {
            label      => "Address Book",
            mode       => 'list',
            args       => { _type => 'notification' },
            order      => 10000,
            permission => 'edit_notifications',
            view       => "blog",
            condition  => sub {
                return $app->config->EnableAddressBook;
            },
        },

        'design:template' => {
            label             => "Templates",
            mode              => 'list',
            args              => { _type => 'template' },
            order             => 100,
            permission        => 'edit_templates',
            system_permission => 'edit_templates',
        },
        'design:widgets' => {
            label             => 'Widgets',
            mode              => 'list_widget',
            order             => 200,
            permission        => 'edit_templates',
            system_permission => "edit_templates",
        },

        'prefs:general' => {
            label      => "General",
            order      => 100,
            mode       => "cfg_system",
            view       => "system",
            permission => "administer",
        },
        'prefs:user' => {
            label      => "User",
            order      => 200,
            mode       => "cfg_system_users",
            view       => "system",
            permission => "administer",
        },
        'prefs:feedback' => {
            label      => "Feedback",
            order      => 300,
            mode       => "cfg_system_feedback",
            view       => "system",
            permission => "administer",
        },
        'prefs:settings' => {
            label      => "General",
            mode       => 'cfg_prefs',
            order      => 100,
            permission => 'administer_blog,edit_config,set_publish_paths',
            system_permission => 'administer',
            view              => "blog",
        },
        'prefs:publishing' => {
            label      => "Publishing",
            mode       => 'cfg_archives',
            order      => 110,
            permission => 'administer_blog,edit_config,set_publish_paths',
            system_permission => 'administer',
            view              => "blog",
        },
        'prefs:entry' => {
            label      => "Entry",
            mode       => 'cfg_entry',
            order      => 120,
            permission => 'administer_blog,edit_config,set_publish_paths',
            system_permission => 'administer',
            view              => "blog",
        },
        'prefs:comment' => {
            label      => "Comment",
            mode       => 'cfg_comments',
            order      => 130,
            permission => 'administer_blog,edit_config,set_publish_paths',
            system_permission => 'administer',
            view              => "blog",
        },
        'prefs:trackback' => {
            label      => "TrackBack",
            mode       => 'cfg_trackbacks',
            order      => 140,
            permission => 'administer_blog,edit_config,set_publish_paths',
            system_permission => 'administer',
            view              => "blog",
        },
        'prefs:registration' => {
            label      => "Registration",
            mode       => 'cfg_registration',
            order      => 150,
            permission => 'administer_blog,edit_config,set_publish_paths',
            system_permission => 'administer',
            view              => "blog",
        },
        'prefs:spam' => {
            label      => "Spam",
            mode       => 'cfg_spam',
            order      => 160,
            permission => 'administer_blog,edit_config,set_publish_paths',
            system_permission => 'administer',
            view              => "blog",
        },
        'prefs:web_services' => {
            label      => "Web Services",
            mode       => 'cfg_web_services',
            order      => 170,
            permission => 'administer_blog,edit_config,set_publish_paths',
            system_permission => 'administer',
            view              => "blog",
        },
        'prefs:ip_info' => {
            label      => "IP Banning",
            mode       => 'list',
            args       => { _type => 'banlist' },
            order      => 180,
            permission => 'manage_feedback',
            condition  => sub {
                $app->config->ShowIPInformation;
            },
            view => "blog",
        },

        'tools:plugins' => {
            label             => "Plugins",
            order             => 100,
            mode              => "cfg_plugins",
            permission        => "administer_blog",
            system_permission => "manage_plugins",
        },
        'tools:activity_log' => {
            label             => "Activity Log",
            order             => 200,
            mode              => "view_log",
            permission        => "view_blog_log",
            system_permission => "view_log",
        },
        'tools:import' => {
            label      => "Import",
            order      => 400,
            mode       => "start_import",
            view       => "blog",
            permission => "administer_blog",
        },
        'tools:export' => {
            label      => "Export",
            order      => 500,
            mode       => "start_export",
            view       => "blog",
            permission => "administer_blog",
        },
        'tools:backup' => {
            label      => "Backup",
            order      => 600,
            mode       => "start_backup",
            permission => "administer_blog",
        },
        'tools:restore' => {
            label      => "Restore",
            order      => 700,
            mode       => "start_restore",
            permission => "administer_blog",
            view       => "system",
        },
        'tools:system_information' => {
            label => "System Information",
            order => 800,
            mode  => "tools",
            view  => "system",
        },

        # System menu which is actually separate
        # in the CMS navigation
        'system' => {
            label => "System Overview",
            mode  => 'dashboard',
            order => 10000,
        },
        'system:user' => {
            label             => "Users",
            mode              => 'list_authors',
            order             => 100,
            permission        => 'administer_blog',
            system_permission => 'administer',
        },
        'system:blog' => {
            label => "Blogs",
            mode  => 'list_blogs',
            order => 200,
        },
        'system:template' => {
            label             => "Global Templates",
            mode              => 'list_template',
            order             => 250,
            system_permission => 'edit_templates',
        },
        'system:settings' => {
            label             => "Settings",
            mode              => 'cfg_system',
            order             => 300,
            system_permission => 'administer',
        },
        'system:plugins' => {
            label             => "Plugins",
            mode              => 'cfg_plugins',
            order             => 400,
            system_permission => 'manage_plugins',
        },
        'system:log' => {
            label             => "Activity Log",
            mode              => 'view_log',
            order             => 500,
            system_permission => 'view_log',
        },
        'system:tools' => {
            label             => "Tools",
            mode              => 'tools',
            order             => 600,
            system_permission => 'administer',
        },
    };
}

sub init_core_callbacks {
    my $app = shift;
    my $pkg = 'cms_';
    my $pfx = '$Core::MT::CMS::';
    $app->_register_core_callbacks(
        {

            # notification callbacks
            $pkg
                . 'save_permission_filter.notification' =>
                "${pfx}AddressBook::can_save",
            $pkg
                . 'save_filter.notification' =>
                "${pfx}AddressBook::save_filter",
            $pkg
                . 'post_delete.notification' =>
                "${pfx}AddressBook::post_delete",

            # banlist callbacks
            $pkg
                . 'save_permission_filter.banlist' =>
                "${pfx}BanList::can_save",
            $pkg . 'save_filter.banlist' => "${pfx}BanList::save_filter",

            # associations
            $pkg
                . 'delete_permission_filter.association' =>
                "${pfx}User::can_delete_association",

            # user callbacks
            $pkg . 'edit.author'                   => "${pfx}User::edit",
            $pkg . 'view_permission_filter.author' => "${pfx}User::can_view",
            $pkg . 'save_permission_filter.author' => "${pfx}User::can_save",
            $pkg
                . 'delete_permission_filter.author' =>
                "${pfx}User::can_delete",
            $pkg . 'save_filter.author' => "${pfx}User::save_filter",
            $pkg . 'pre_save.author'    => "${pfx}User::pre_save",
            $pkg . 'post_save.author'   => "${pfx}User::post_save",
            $pkg . 'post_delete.author' => "${pfx}User::post_delete",

            # blog callbacks
            $pkg . 'edit.blog'                   => "${pfx}Blog::edit",
            $pkg . 'view_permission_filter.blog' => "${pfx}Blog::can_view",
            $pkg . 'save_permission_filter.blog' => "${pfx}Blog::can_save",
            $pkg
                . 'delete_permission_filter.blog' => "${pfx}Blog::can_delete",
            $pkg . 'pre_save.blog'    => "${pfx}Blog::pre_save",
            $pkg . 'post_save.blog'   => "${pfx}Blog::post_save",
            $pkg . 'save_filter.blog' => "${pfx}Blog::save_filter",
            $pkg . 'post_delete.blog' => "${pfx}Blog::post_delete",

            # folder callbacks
            $pkg . 'edit.folder' => "${pfx}Folder::edit",
            $pkg
                . 'view_permission_filter.folder' => "${pfx}Folder::can_view",
            $pkg
                . 'save_permission_filter.folder' => "${pfx}Folder::can_save",
            $pkg
                . 'delete_permission_filter.folder' =>
                "${pfx}Folder::can_delete",
            $pkg . 'pre_save.folder'    => "${pfx}Folder::pre_save",
            $pkg . 'post_save.folder'   => "${pfx}Folder::post_save",
            $pkg . 'save_filter.folder' => "${pfx}Folder::save_filter",
            $pkg . 'post_delete.folder' => "${pfx}Folder::post_delete",

            # category callbacks
            $pkg . 'edit.category' => "${pfx}Category::edit",
            $pkg
                . 'view_permission_filter.category' =>
                "${pfx}Category::can_view",
            $pkg
                . 'save_permission_filter.category' =>
                "${pfx}Category::can_save",
            $pkg
                . 'delete_permission_filter.category' =>
                "${pfx}Category::can_delete",
            $pkg . 'pre_save.category'    => "${pfx}Category::pre_save",
            $pkg . 'post_save.category'   => "${pfx}Category::post_save",
            $pkg . 'save_filter.category' => "${pfx}Category::save_filter",
            $pkg . 'post_delete.category' => "${pfx}Category::post_delete",

            # comment callbacks
            $pkg . 'edit.comment' => "${pfx}Comment::edit",
            $pkg
                . 'view_permission_filter.comment' =>
                "${pfx}Comment::can_view",
            $pkg
                . 'save_permission_filter.comment' =>
                "${pfx}Comment::can_save",
            $pkg
                . 'delete_permission_filter.comment' =>
                "${pfx}Comment::can_delete",
            $pkg . 'pre_save.comment'    => "${pfx}Comment::pre_save",
            $pkg . 'post_save.comment'   => "${pfx}Comment::post_save",
            $pkg . 'post_delete.comment' => "${pfx}Comment::post_delete",

            # commenter callbacks
            $pkg . 'edit.commenter' => "${pfx}Comment::edit_commenter",
            $pkg
                . 'view_permission_filter.commenter' =>
                "${pfx}Comment::can_view_commenter",
            $pkg
                . 'delete_permission_filter.commenter' =>
                "${pfx}Comment::can_delete_commenter",

            # entry callbacks
            $pkg . 'edit.entry'                   => "${pfx}Entry::edit",
            $pkg . 'view_permission_filter.entry' => "${pfx}Entry::can_view",
            $pkg
                . 'delete_permission_filter.entry' =>
                "${pfx}Entry::can_delete",
            $pkg . 'pre_save.entry'    => "${pfx}Entry::pre_save",
            $pkg . 'post_save.entry'   => "${pfx}Entry::post_save",
            $pkg . 'post_delete.entry' => "${pfx}Entry::post_delete",

            # page callbacks
            $pkg . 'edit.page'                   => "${pfx}Page::edit",
            $pkg . 'view_permission_filter.page' => "${pfx}Page::can_view",
            $pkg
                . 'delete_permission_filter.page' => "${pfx}Page::can_delete",
            $pkg . 'pre_save.page'    => "${pfx}Page::pre_save",
            $pkg . 'post_save.page'   => "${pfx}Page::post_save",
            $pkg . 'post_delete.page' => "${pfx}Page::post_delete",

            # ping callbacks
            $pkg . 'edit.ping' => "${pfx}TrackBack::edit",
            $pkg
                . 'view_permission_filter.ping' =>
                "${pfx}TrackBack::can_view",
            $pkg
                . 'save_permission_filter.ping' =>
                "${pfx}TrackBack::can_save",
            $pkg
                . 'delete_permission_filter.ping' =>
                "${pfx}TrackBack::can_delete",
            $pkg . 'pre_save.ping'    => "${pfx}TrackBack::pre_save",
            $pkg . 'post_save.ping'   => "${pfx}TrackBack::post_save",
            $pkg . 'post_delete.ping' => "${pfx}TrackBack::post_delete",

            # template callbacks
            $pkg . 'edit.template' => "${pfx}Template::edit",
            $pkg
                . 'view_permission_filter.template' =>
                "${pfx}Template::can_view",
            $pkg
                . 'save_permission_filter.template' =>
                "${pfx}Template::can_save",
            $pkg
                . 'delete_permission_filter.template' =>
                "${pfx}Template::can_delete",
            $pkg . 'pre_save.template'    => "${pfx}Template::pre_save",
            $pkg . 'post_save.template'   => "${pfx}Template::post_save",
            $pkg . 'post_delete.template' => "${pfx}Template::post_delete",
            'restore' => "${pfx}Template::restore_widgetmanagers",

            # tags
            $pkg . 'delete_permission_filter.tag' => "${pfx}Tag::can_delete",
            $pkg . 'post_delete.tag'              => "${pfx}Tag::post_delete",

            # junk-related callbacks
            #'HandleJunk' => \&_builtin_spam_handler,
            #'HandleNotJunk' => \&_builtin_spam_unhandler,
            $pkg . 'not_junk_test' => "${pfx}Common::not_junk_test",

            # assets
            $pkg . 'edit.asset'                   => "${pfx}Asset::edit",
            $pkg . 'view_permission_filter.asset' => "${pfx}Asset::can_view",
            $pkg
                . 'delete_permission_filter.asset' =>
                "${pfx}Asset::can_delete",
            $pkg . 'pre_save.asset'     => "${pfx}Asset::pre_save",
            $pkg . 'post_save.asset'    => "${pfx}Asset::post_save",
            $pkg . 'post_delete.asset'  => "${pfx}Asset::post_delete",
            'template_param.edit_asset' => "${pfx}Asset::template_param_edit",
        }
    );
}

sub user_can_admin_commenters {
    my $app   = shift;
    my $perms = $app->permissions;
    $app->user->is_superuser()
        || ( $perms
        && ( $perms->can_administer_blog || $perms->can_manage_feedback ) );
}

sub validate_magic {
    my $app = shift;
    if ( my $feed_token = $app->param('feed_token') ) {
        return unless $app->user;
        my $pw = $app->user->api_password;
        return undef if ( $pw || '' ) eq '';
        my $auth_token = perl_sha1_digest_hex( 'feed:' . $pw );
        return $feed_token eq $auth_token;
    }
    else {
        return $app->SUPER::validate_magic(@_);
    }
}

sub is_authorized {
    my $app     = shift;
    my $blog_id = $app->param('blog_id');
    $app->permissions(undef);
    return 1 unless $blog_id;
    return unless my $user = $app->user;
    my $perms = $app->permissions( $user->permissions($blog_id) );
    $perms
        ? 1
        : $app->error(
        $app->translate("You are not authorized to log in to this blog.") );
}

sub set_default_tmpl_params {
    my $app = shift;
    $app->SUPER::set_default_tmpl_params(@_);
    my ($tmpl) = @_;
    my $param = {};

    my $mode      = $app->mode;
    my $auth_mode = $app->config('AuthenticationModule');
    my ($pref) = split /\s+/, $auth_mode;

    # TODO - remove after testing or after new IA is defined
    $param->{app_layout_fixed} = 0;
    $param->{athena_nav}       = 1;

    $param->{"auth_mode_$pref"} = 1;
    $param->{mt_news}           = $app->config('NewsURL');
    $param->{mt_support}        = $app->config('SupportURL');
    my $lang = lc MT->current_language || 'en_us';
    $param->{language_id} = ( $lang !~ /en[_-]us/ ) ? $lang : '';
    $param->{mode} = $app->mode;

    my $blog_id = $app->param('blog_id') || 0;
    my $blog;
    my $blog_class = $app->model('blog');
    $blog ||= $blog_class->load($blog_id) if $blog_id;
    if ( my $auth = $app->user ) {
        $param->{is_administrator} = $auth->is_superuser;
        $param->{can_create_blog}  = $auth->can_create_blog;
        $param->{can_view_log} ||= $auth->can_view_log;
        $param->{can_manage_plugins}    = $auth->can_manage_plugins;
        $param->{can_edit_templates}    = $auth->can_edit_templates;
        $param->{can_publish_feedbacks} = $auth->is_superuser;
        $param->{can_search_replace}    = $auth->is_superuser;
        $param->{has_authors_button}    = $auth->is_superuser;
        $param->{author_id}             = $auth->id;
        $param->{author_name}           = $auth->name;
        $param->{author_display_name}   = $auth->nickname || $auth->name;
    }

    if ( my $perms = $app->permissions ) {
        my $perm_hash = $perms->to_hash;
        foreach my $perm_name ( keys %$perm_hash ) {
            my $perm_token = $perm_name;
            $perm_token =~ s/^permission\.//;
            $param->{$perm_token} = $perm_hash->{$perm_name}
                if defined $perm_hash->{$perm_name};
        }
        $param->{can_edit_entries} 
            = $param->{can_create_post}
            || $param->{can_edit_all_entries}
            || $param->{can_publish_post};
        $param->{can_search_replace} = $param->{can_edit_all_posts};
        $param->{can_edit_authors}   = $param->{can_administer_blog};
        $param->{can_access_assets} 
            = $param->{can_create_post}
            || $param->{can_edit_all_posts}
            || $param->{can_edit_assets};
        $param->{can_edit_commenters} = $param->{can_manage_feedback};
        $param->{has_manage_label} 
            = $param->{can_edit_templates}
            || $param->{can_administer_blog}
            || $param->{can_edit_categories}
            || $param->{can_edit_config}
            || $param->{can_edit_tags}
            || $param->{can_set_publish_paths}
            || $param->{show_ip_info};
        $param->{has_posting_label} 
            = $param->{can_create_post}
            || $param->{can_edit_entries}
            || $param->{can_access_assets};
        $param->{has_community_label} = $param->{can_edit_entries}
            || $param->{can_edit_notifications};
        $param->{can_publish_feedbacks} 
            = $param->{can_manage_feedback}
            || $param->{can_publish_post}
            || $param->{can_edit_all_posts};
        $param->{can_view_log}     = $param->{can_view_blog_log};
        $param->{can_publish_post} = $param->{can_publish_post}
            || $param->{can_edit_all_posts};
        $param->{show_ip_info} = $app->config->ShowIPInformation
            && $param->{can_manage_feedback};
    }

    my $static_app_url = $app->static_path;
    $param->{help_url} = $app->help_url() || $static_app_url . 'docs/';

    $param->{show_ip_info} ||= $app->config('ShowIPInformation');
    my $type = $app->param('_type') || '';

    $param->{ "mode_$mode" . ( $type ? "_$type" : '' ) } = 1;
    $param->{return_args} ||= $app->make_return_args;
    $tmpl->param($param);
}

sub build_page {
    my $app = shift;
    my ( $page, $param ) = @_;
    $param ||= {};

    my $blog_id = $app->param('blog_id') || 0;
    my $blog;
    my $blog_class = $app->model('blog');
    $blog ||= $blog_class->load($blog_id) if $blog_id;
    if ( $blog_id && $page ne 'login.tmpl' ) {
        if ($blog) {
            $param->{blog_name}         = $blog->name;
            $param->{blog_id}           = $blog->id;
            $param->{blog_url}          = $blog->site_url;
            $param->{blog_template_set} = $blog->template_set;
        }
        else {
            $app->error( $app->translate( "No such blog [_1]", $blog_id ) );
        }
    }
    if ( $page ne 'login.tmpl' ) {
        if ( ref $app eq 'MT::App::CMS' ) {
            $param->{system_overview_nav} = 1
                unless $blog_id
                    || exists $param->{system_overview_nav}
                    || $param->{no_breadcrumbs};
            $param->{quick_search} = 1 unless defined $param->{quick_search};
        }
    }

    $app->build_blog_selector($param);
    $app->build_menus($param);
    if ( !ref($page)
        || ( $page->isa('MT::Template') && !$page->param('page_actions') ) )
    {
        # Using a sub here to delay the loading of page actions, since not all
        # templates actually utilize them.
        $param->{page_actions} ||= sub { $app->page_actions( $app->mode ) };
    }

    $app->SUPER::build_page( $page, $param );
}

sub build_blog_selector {
    my $app = shift;
    my ($param) = @_;

    return if exists $param->{top_blog_loop};

    my $blog = $app->blog;
    my $blog_id = $blog->id if $blog;
    $param->{dynamic_all} = $blog->custom_dynamic_templates eq 'all' if $blog;

    my $blog_class = $app->model('blog');
    my $auth = $app->user or return;

    # Any access to a blog will put it on the top of your
    # recently used blogs list (the blog selector)
    $app->add_to_favorite_blogs($blog_id) if $blog_id;

    my %args;
    $args{join} = MT::Permission->join_on( 'blog_id',
        { author_id => $auth->id, permissions => { not => "'comment'" } } );
    $args{limit} = 11;    # don't load more than 11
    my @blogs = $blog_class->load( undef, \%args );

    my @fav_blogs = @{ $auth->favorite_blogs || [] };
    @fav_blogs = grep { $_ != $blog_id } @fav_blogs if $blog_id;

    # Special case for when a user only has access to a single blog.
    if (   ( !defined( $app->param('blog_id') ) )
        && ( @blogs == 1 )
        && ( scalar @fav_blogs <= 1 ) )
    {

        # User only has visibility to a single blog. Don't
        # bother giving them a dashboard link for 'all blogs', or
        # to 'select a blog'.
        $param->{single_blog_mode} = 1;
        my $blog = $blogs[0];
        $blog_id = $blog->id;
        my $perms = MT::Permission->load(
            {   blog_id   => $blog_id,
                author_id => $auth->id
            }
        );
        if ( !$app->blog ) {
            if ( $app->mode eq 'dashboard' ) {
                $app->param( 'blog_id', $blog_id );
                $param->{blog_id}   = $blog_id;
                $param->{blog_name} = $blog->name;
                $app->permissions($perms);
                $app->blog($blog);
            }
            else {
                @fav_blogs = ($blog_id);
                $blog_id   = undef;
            }
        }
    }
    elsif ( @blogs && ( @blogs <= 10 ) ) {

        # This user only has visibility to 10 or fewer blogs;
        # no need to reference their 'favorite' blogs list.
        my @ids = map { $_->id } @blogs;
        if ($blog_id) {
            @ids = grep { $_ != $blog_id } @ids;
        }
        @fav_blogs = @ids;
        if ( $auth->is_superuser ) {

            # Better check to see if there are more than
            # 10 blogs in the system; if so, a superuser
            # will still want the 'Select a blog...' chooser.
            # Otherwise, hide it.
            my $all_blog_count = $blog_class->count();
            if ( $all_blog_count < 11 ) {
                $param->{selector_hide_chooser} = 1;
            }
        }
        else {

            # This user is not a superuser and only has
            # 10 blogs, so they don't need a 'select blog'
            # link...
            $param->{selector_hide_chooser} = 1;
        }
    }
    $param->{selector_hide_chooser} ||= 0;

    # Logic for populating the blog selector control
    #   * Pull list of 'favorite blogs' from user record
    #   * Load all of those blogs so we can display them
    #   * Exclude the current blog from the favorite list so it isn't
    #     shown twice.
    @blogs = $blog_class->load( { id => \@fav_blogs } ) if @fav_blogs;
    my %blogs = map { $_->id => $_ } @blogs;
    @blogs = ();
    foreach my $id (@fav_blogs) {
        push @blogs, $blogs{$id} if $blogs{$id};
    }

    my @data;
    if (@blogs) {
        my @perms = grep { !$_->is_empty } MT::Permission->load(
            {   author_id => $auth->id,
                blog_id   => \@fav_blogs,
            }
        );
        my %perms = map { $_->blog_id => $_ } @perms;
        for my $blog (@blogs) {
            my $perm = $perms{ $blog->id };
            next
                unless $auth->is_superuser || ( $perm && !$perm->is_empty );
            push @data,
                {
                top_blog_id   => $blog->id,
                top_blog_name => $blog->name
                };
            $data[-1]{top_blog_selected} = 1
                if $blog_id && ( $blog->id == $blog_id );
        }
    }
    $param->{top_blog_loop} = \@data;
    if ( !$app->user->can_create_blog
        && ( $param->{single_blog_mode} || scalar(@data) <= 1 ) )
    {
        $param->{no_submenu} = 1;
    }
}

sub build_menus {
    my $app = shift;
    my ($param) = @_;
    return if exists $param->{top_nav_loop};

    my $menus   = $app->registry('menus');
    my $blog    = $app->blog;
    my $blog_id = $blog->id if $blog;

    my @top_ids = grep { !/:/ } keys %$menus;
    my @top;
    my @sys;
    my $user = $app->user
        or return;
    my $perms = $app->permissions || $user->permissions;
    my $view = $blog_id ? "blog" : "system";

    my $hide_disabled_options = $app->config('HideDisabledMenus') || 0;

    my $admin = $user->is_superuser()
        ;    # || ($perms && $perms->has('administer_blog'));

    foreach my $id (@top_ids) {
        my $menu = $menus->{$id};
        next if $menu->{view} && $menu->{view} ne $view;
        if ( my $cond = $menu->{condition} ) {
            if ( !ref($cond) ) {
                $cond = $menu->{condition} = $app->handler_to_coderef($cond);
            }
            next unless $cond->();
        }

        $menu->{allowed} = 1;
        $menu->{'id'} = $id;

        my @sub_ids = grep {m/^$id:/} keys %$menus;
        my @sub;
        foreach my $sub_id (@sub_ids) {
            my $sub = $menus->{$sub_id};
            next
                if $sub->{view}
                    && ( $sub->{view} ne $view );
            $sub->{'id'} = $sub_id;
            if ( my $cond = $sub->{condition} ) {
                if ( !ref($cond) ) {
                    $cond = $sub->{condition}
                        = $app->handler_to_coderef($cond);
                }
                next unless $cond->();
            }
            push @sub, $sub;
        }

        if (my $p
            = $blog_id
            ? $menu->{permission} || $menu->{system_permission}
            : $menu->{system_permission}
            || $menu->{permission}
            )
        {
            my $allowed = 0;
            my @p = split /,/, $p;
            foreach my $p (@p) {
                my $perm = 'can_' . $p;
                $allowed = 1, last if ( $perms && $perms->$perm() ) || $admin;
            }
            $menu->{allowed} = $allowed;
        }
        elsif ( !$perms && !$blog_id ) {
            $menu->{allowed} = 0 if $menu->{system_permission} && !$admin;
        }

        next if $hide_disabled_options && ( !$menu->{allowed} );

        if (@sub) {
            if ( $id eq 'system' ) {
                push @sys, $menu;
            }
            else {
                push @top, $menu;
            }

            my $has_sub = 0;
            foreach my $sub (@sub) {
                my $sys_only = $sub->{id} =~ m/^system:/;
                $sub->{allowed} = 1;
                if ( $sub->{mode} ) {
                    $sub->{link} = $app->uri(
                        mode => $sub->{mode},
                        args => {
                            %{ $sub->{args} || {} },
                            (   $blog_id && !$sys_only
                                ? ( blog_id => $blog_id )
                                : ()
                            )
                        }
                    );
                }
                $sub->{allowed} = 0
                    if $sub->{view} && ( $sub->{view} ne $view );
                if ( $sub->{allowed} ) {
                    my $sub_perms
                        = ( $sys_only || ( $sub->{view} || '' ) eq 'system' )
                        ? $app->user->permissions(0)
                        : $perms;
                    if ($sub_perms
                        && (my $p
                            = $blog_id
                            ? $sub->{permission} || $sub->{system_permission}
                            : $sub->{system_permission}
                            || $sub->{permission}
                        )
                        )
                    {
                        my $allowed = 0;
                        my @p = split /,/, $p;
                        foreach my $p (@p) {
                            my $perm = 'can_' . $p;
                            $allowed = 1, last
                                if ( $sub_perms && $sub_perms->$perm() )
                                || $admin;
                        }
                        $sub->{allowed} = $allowed;
                    }
                    elsif ( !$sub_perms && !$blog_id ) {
                        $sub->{allowed} = 0
                            if ( $sub->{system_permission}
                            || $sub->{permission} )
                            && !$admin;
                    }
                    $has_sub = 1 if $sub->{allowed};
                }
            }
            if ( $menu->{mode} ) {
                my $sys_only = 1 if $menu->{id} eq 'system';
                $menu->{link} = $app->uri(
                    mode => $menu->{mode},
                    args => {
                        %{ $menu->{args} || {} },
                        (   $blog_id
                                && !$sys_only ? ( blog_id => $blog_id ) : ()
                        )
                    }
                );
            }
            @sub = sort { $a->{order} <=> $b->{order} } @sub;

            @sub = grep { $_->{allowed} } @sub if $hide_disabled_options;
            if ( !$menu->{mode} ) {
                $menu->{link} = $sub[0]->{link};
            }
            $menu->{sub_nav_loop} = \@sub;

            if ( $menu->{allowed} ) {
                $menu->{allowed} = 0 unless $has_sub;
            }
        }
    }

    @top = grep { $_->{allowed} } @top if $hide_disabled_options;
    @sys = grep { $_->{allowed} } @sys if $hide_disabled_options;
    @top = sort { $a->{order} <=> $b->{order} } @top;
    $param->{top_nav_loop} = \@top;
    $param->{sys_nav_loop} = \@sys;
}

sub return_to_dashboard {
    my $app = shift;
    my (%param) = @_;
    $param{redirect} = 1 unless %param;
    my $blog_id = $app->param('blog_id');
    $param{blog_id} = $blog_id if $blog_id;
    return $app->redirect(
        $app->uri( mode => 'dashboard', args => \%param ) );
}

sub list_pref {
    my $app      = shift;
    my ($list)   = @_;
    my $updating = $app->mode eq 'update_list_prefs';
    unless ($updating) {
        my $pref = $app->request("list_pref_$list");
        return $pref if defined $pref;
    }

    my $cookie = $app->cookie_val('mt_list_pref') || '';
    my $mode = $app->mode;

    # defaults:
    my $d = $app->config->DefaultListPrefs || {};
    my %default = (
        Rows       => 25,
        Format     => 'Compact',
        SortOrder  => 'Ascend',      # Ascend|Descend
        Button     => 'Above',       # Above|Below|Both
        DateFormat => 'Relative',    # Relative|Full
    );
    if ( ( $list eq 'comment' ) || ( $list eq 'ping' ) ) {
        $default{Format} = 'expanded';
    }
    $default{$_} = lc( $d->{$_} ) for keys %$d;
    my $list_pref;
    if ( $list eq 'main_menu' ) {
        $list_pref = {
            'sort' => 'name',
            order  => $default{SortOrder} || 'ascend',
            view   => $default{Format} || 'compact',
            dates  => $default{DateFormat} || 'relative',
        };
    }
    else {
        $list_pref = {
            rows  => $default{Rows}       || 25,
            view  => $default{Format}     || 'compact',
            bar   => $default{Button}     || 'above',
            dates => $default{DateFormat} || 'relative',
        };
    }
    my @list_prefs = split /;/, $cookie;
    my $new_cookie = '';
    foreach my $pref (@list_prefs) {
        my ( $name, $prefs ) = $pref =~ m/^(\w+):(.*)$/;
        next unless $name && $prefs;
        if ( $name eq $list ) {
            my @prefs = split /,/, $prefs;
            foreach (@prefs) {
                my ( $k, $v ) = split /=/;
                $list_pref->{$k} = $v if exists $list_pref->{$k};
            }
        }
        else {
            $new_cookie .= ( $new_cookie ne '' ? ';' : '' ) . $pref;
        }
    }

    if ($updating) {
        my $updated = 0;
        if ( my $limit = $app->param('limit') ) {
            $limit = 20 if $limit eq 'none';
            $list_pref->{rows} = $limit > 0 ? $limit : 20;
            $updated = 1;
        }
        if ( my $view = $app->param('verbosity') ) {
            if ( $view =~ m!^compact|expanded$! ) {
                $list_pref->{view} = $view;
                $updated = 1;
            }
        }
        if ( my $bar = $app->param('actions') ) {
            if ( $bar =~ m!^above|below|both$! ) {
                $list_pref->{bar} = $bar;
                $updated = 1;
            }
        }
        if ( my $ord = $app->param('order') ) {
            if ( $ord =~ m!^ascend|descend$! ) {
                $list_pref->{order} = $ord;
                $updated = 1;
            }
        }
        if ( my $sort = $app->param('sort') ) {
            if ( $sort =~ m!^name|created|updated$! ) {
                $list_pref->{'sort'} = $sort;
                $updated = 1;
            }
        }
        if ( my $dates = $app->param('dates') ) {
            if ( $dates =~ m!^relative|full$! ) {
                $list_pref->{'dates'} = $dates;
                $updated = 1;
            }
        }

        if ($updated) {
            my @list_prefs;
            foreach ( keys %$list_pref ) {
                push @list_prefs, $_ . '=' . $list_pref->{$_};
            }
            my $prefs = join ',', @list_prefs;
            $new_cookie
                .= ( $new_cookie ne '' ? ';' : '' ) . $list . ':' . $prefs;
            $app->bake_cookie(
                -name    => 'mt_list_pref',
                -value   => $new_cookie,
                -expires => '+10y'
            );
        }
    }

    if ( $list_pref->{rows} ) {
        $list_pref->{ "limit_" . $list_pref->{rows} } = $list_pref->{rows};
    }
    if ( $list_pref->{view} ) {
        $list_pref->{ "view_" . $list_pref->{view} } = 1;
    }
    if ( $list_pref->{dates} ) {
        $list_pref->{ "dates_" . $list_pref->{dates} } = 1;
    }
    if ( $list_pref->{bar} ) {
        if ( lc $list_pref->{bar} eq 'both' ) {
            $list_pref->{"position_actions_both"}   = 1;
            $list_pref->{"position_actions_top"}    = 1;
            $list_pref->{"position_actions_bottom"} = 1;
        }
        elsif ( lc $list_pref->{bar} eq 'below' ) {
            $list_pref->{"position_actions_bottom"} = 1;
        }
        elsif ( lc $list_pref->{bar} eq 'above' ) {
            $list_pref->{"position_actions_top"} = 1;
        }
    }
    if ( $list_pref->{'sort'} ) {
        $list_pref->{ 'sort_' . $list_pref->{'sort'} } = 1;
    }
    if ( $list_pref->{'order'} ) {
        $list_pref->{ 'order_' . $list_pref->{'order'} } = 1;
    }
    $app->request( "list_pref_$list", $list_pref );
}

sub make_feed_link {
    my $app = shift;
    my ( $view, $params ) = @_;
    my $user = $app->user;
    return if ( $user->api_password || '' ) eq '';

    $params ||= {};
    $params->{view}     = $view;
    $params->{username} = $user->name;
    $params->{token} = perl_sha1_digest_hex( 'feed:' . $user->api_password );
    $app->base
        . $app->mt_path
        . $app->config('ActivityFeedScript')
        . $app->uri_params( args => $params );
}

sub show_error {
    my $app = shift;
    my ($param) = @_;

    # handle legacy scalar error string signature
    $param = { error => $param } unless ref($param) eq 'HASH';

    my $mode = $app->mode;
    if ( $mode eq 'rebuild' ) {

        my $r = MT::Request->instance;
        if ( my $tmpl = $r->cache('build_template') ) {

            # this is the template that likely caused the rebuild error
            my $tmpl_edit_link = $app->uri(
                        mode => 'view',
                        args => {
                            blog_id => $tmpl->blog_id,
                            '_type' => 'template',
                            id      => $tmpl->id
                        }
                    );

            if ( $app->param('fs') ) {
                $param->{fs} = 1;
                if ( exists $app->{goback} ) {
                    $param->{goback} = "window.location='" . $app->{goback} . "'";
                    if ( $tmpl_edit_link ne $app->{goback} ) {
                        push @{ $param->{button_loop} ||= [] },
                          {
                            link => $tmpl_edit_link,
                            label => $app->translate("Edit Template"),
                          };
                    }
                }
                else {
                    $param->{goback} = "window.location='$tmpl_edit_link'";
                }
            }
            else {
                push @{ $param->{button_loop} ||= [] },
                  {
                    link => $tmpl_edit_link,
                    label => $app->translate("Edit Template"),
                  };
            }
        }

        if ( !exists( $param->{goback} ) && exists( $app->{goback} ) ) {
            $param->{goback} = "window.location='" . $app->{goback} . "'";
        }
        else {
            my $blog_id = $app->param('blog_id');
            my $url     = $app->uri(
                mode => 'rebuild_confirm',
                args => { blog_id => $blog_id }
            );
            $param->{goback} ||= qq{window.location='$url'};
        }
        $param->{value} ||= $app->{value} || $app->translate('Go Back');
    }

    return $app->SUPER::show_error($param);
}

sub load_default_entry_prefs {
    my $app = shift;
    my $prefs;
    require MT::Permission;
    my $blog_id;
    $blog_id = $app->blog->id if $app->blog;
    my $perm
        = MT::Permission->load( { blog_id => $blog_id, author_id => 0 } );
    my %default = %{ $app->config->DefaultEntryPrefs };
    %default = map { lc $_ => $default{$_} } keys %default;
    if ( $perm && $perm->entry_prefs ) {
        $prefs = $perm->entry_prefs;
    }
    else {
        if ( lc( $default{type} ) eq 'custom' ) {
            my %map = (
                Category   => 'category',
                Excerpt    => 'excerpt',
                Keywords   => 'keywords',
                Tags       => 'tags',
                Publishing => 'publishing',
                Feedback   => 'feedback',
                Assets     => 'assets',
            );
            my @p;
            foreach my $p ( keys %map ) {
                push @p, $map{$p} . ':' . $default{ lc $p }
                    if $default{ lc $p };
            }
            $prefs = join ',', @p;
            $prefs ||= 'Custom';
        }
        elsif ( lc( $default{type} ) ne 'default' ) {
            $prefs = 'Advanced';
        }
        $default{button} = 'Bottom' if lc( $default{button} ) eq 'below';
        $default{button} = 'Top'    if lc( $default{button} ) eq 'above';
        $prefs .= '|' . $default{button} if $prefs;
    }
    $prefs ||= 'Default|Bottom';
    return $prefs;
}

sub load_template_prefs {
    my $app = shift;
    my ($prefs) = @_;
    my %param;

    if ( !$prefs ) {
        $prefs = '';
    }
    my @p = split /,/, $prefs;
    for my $p (@p) {
        if ( $p =~ m/^(.+?):(\d+)$/ ) {
            $param{ 'disp_prefs_height_' . $1 } = $2;
        }
    }
    \%param;
}

sub _parse_entry_prefs {
    my $app = shift;
    my ( $prefs, $param, $fields ) = @_;

    my @p = split /,/, $prefs;
    for my $p (@p) {
        if ( $p =~ m/^(.+?):(\d+)$/ ) {
            my ( $name, $num ) = ( $1, $2 );
            if ($num) {
                $param->{ 'disp_prefs_height_' . $name } = $num;
            }
            $param->{ 'disp_prefs_show_' . $name } = 1;
            push @$fields, { name => $name };
        }
        else {
            $p = 'Default' if lc($p) eq 'basic';
            if ( ( lc($p) eq 'advanced' ) || ( lc($p) eq 'default' ) ) {
                $param->{ 'disp_prefs_' . $p } = 1;
                foreach my $def (
                    qw( title body category tags keywords feedback publishing assets )
                    )
                {
                    $param->{ 'disp_prefs_show_' . $def } = 1;
                    push @$fields, { name => $def };
                }
                if ( lc($p) eq 'advanced' ) {
                    foreach my $def (qw(excerpt feedback)) {
                        $param->{ 'disp_prefs_show_' . $def } = 1;
                        push @$fields, { name => $def };
                    }
                }
            }
            else {
                $param->{ 'disp_prefs_show_' . $p } = 1;
                push @$fields, { name => $p };
            }
        }
    }
}

sub load_entry_prefs {
    my $app = shift;
    my ($prefs) = @_;
    my %param;
    my $pos;
    my $is_from_db = 1;

    # defaults:
    if ( !$prefs ) {
        $prefs = $app->load_default_entry_prefs;
        ( $prefs, $pos ) = split /\|/, $prefs;
        $is_from_db = 0;
        $app->_parse_entry_prefs( $prefs, \%param, \my @fields );
        $param{disp_prefs_default_fields} = \@fields;
    }
    else {
        ( $prefs, $pos ) = split /\|/, $prefs;
    }
    $app->_parse_entry_prefs( $prefs, \%param, \my @custom_fields );
    if ($is_from_db) {
        my $default_prefs = $app->load_default_entry_prefs;
        ( $default_prefs, my ($default_pos) ) = split /\|/, $default_prefs;
        $pos ||= $default_pos;
        $app->_parse_entry_prefs( $default_prefs, \my %def_param, \my @fields );
        if ( $prefs eq 'Default' ) {
            foreach my $p ( keys %param ) {
                delete $param{$p} if $p =~ m/^disp_prefs_show_/;
            }
        }
        if ( $param{disp_prefs_Default} ) {

            # apply default settings
            %param = ( %def_param, %param );
        }
        $param{disp_prefs_default_fields} = \@fields;
    }
    $pos ||= 'Bottom';
    if (   !exists $param{'disp_prefs_Default'}
        && !exists $param{'disp_prefs_Advanced'} )
    {
        $param{'disp_prefs_Custom'} = 1;
        $param{disp_prefs_custom_fields} = \@custom_fields if @custom_fields;
    }
    if ( lc $pos eq 'both' ) {
        $param{'position_actions_top'}    = 1;
        $param{'position_actions_bottom'} = 1;
        $param{'position_actions_both'}   = 1;
    }
    else {
        $param{ 'position_actions_' . $pos } = 1;
    }
    \%param;
}

sub _convert_word_chars {
    my ( $app, $s ) = @_;

    return '' unless $s;
    return $s if 'utf-8' ne lc( $app->charset );

    my $blog = $app->blog;
    my $smart_replace
        = $blog
        ? $blog->smart_replace
        : MT->config->NwcSmartReplace;
    return $s if $smart_replace == 2;

    require MT::Util;
    return MT::Util::convert_word_chars($s, $smart_replace);
}

sub _translate_naughty_words {
    my ($app, $entry) = @_;
    require MT::Util;
    return MT::Util::translate_naughty_words($entry);
}

sub autosave_session_obj {
    my $app           = shift;
    my ($or_make_one) = @_;
    my $q             = $app->param;
    my $type          = $q->param('_type');
    return unless $type;
    my $id = $q->param('id');
    $id = '0' unless $id;
    my $ident
        = 'autosave' 
        . ':user='
        . $app->user->id
        . ':type='
        . $type . ':id='
        . $id;

    if ( my $blog = $app->blog ) {
        $ident .= ':blog_id=' . $blog->id;
    }
    require MT::Session;
    my $sess_obj = MT::Session->load( { id => $ident, kind => 'AS' } );

    if ( !$sess_obj && $or_make_one ) {

        # autosave is being requested, so provision an object for saving
        $sess_obj = new MT::Session;
        $sess_obj->kind('AS');    # AutoSave record
        $sess_obj->id($ident);
        $sess_obj->start(time);
    }
    return $sess_obj;
}

sub autosave_object {
    my $app = shift;
    my $sess_obj = $app->autosave_session_obj(1) or return;
    $sess_obj->data('');
    my $class = $app->model( $app->param('_type') ) or return;
    my %data = $app->param_hash;
    delete $data{_autosave};
    delete $data{magic_token};

    # my $col_names = $class->column_names;
    # foreach my $c (@$col_names) {
    foreach my $c ( keys %data ) {
        $sess_obj->set( $c, $data{$c} );
    }
    $sess_obj->start(time);
    $sess_obj->save;
    $app->send_http_header("text/javascript+json");
    $app->{no_print_body} = 1;
    require JSON;
    $app->print("true");
}

sub model {
    my $app    = shift;
    my ($type) = @_;
    my $class  = $app->SUPER::model($type)
        or return $app->error(
        $app->translate( "Unknown object type [_1]", $type ) );
    $class;
}
*_load_driver_for = \&model;

sub listify {
    my ($arr) = @_;
    my @ret;
    return unless ref($arr) eq 'ARRAY';
    foreach (@$arr) {
        push @ret, { name => $_ };
    }
    \@ret;
}

sub user_blog_prefs {
    my $app   = shift;
    my $prefs = $app->request('user_blog_prefs');
    return $prefs if $prefs && !$app->param('config_view');

    my $perms = $app->permissions;
    return {} unless $perms;
    my @prefs = split /,/, $perms->blog_prefs || '';
    my %prefs;
    foreach (@prefs) {
        my ( $name, $value ) = split /=/, $_, 2;
        $prefs{$name} = $value;
    }
    my $updated = 0;
    if ( my $view = $app->param('config_view') ) {
        $prefs{'config_view'} = $view;
        $updated = 1;
    }
    if ($updated) {
        my $pref = '';
        foreach ( keys %prefs ) {
            $pref .= ',' if $pref ne '';
            $pref .= $_ . '=' . $prefs{$_};
        }
        $perms->blog_prefs($pref);
        if ( !$perms->blog_id ) {
            my $blog = $app->blog;
            $perms->blog_id( $blog->id ) if $blog;
        }
        $perms->save if $perms->blog_id;
    }
    $app->request( 'user_blog_prefs', \%prefs );
    \%prefs;
}

sub archive_type_sorter {
    my ( $a, $b ) = @_;

    # Logical ordering for exact archive types
    my %order = (
        'Individual' => 1,
        'Page'       => 2,
        'Daily'      => 3,
        'Weekly'     => 4,
        'Monthly'    => 5,
        'Yearly'     => 6
    );
    my $ord_a = $order{ $a->{archive_type} };
    my $ord_b = $order{ $b->{archive_type} };

    if ( defined($ord_a) && defined($ord_b) ) {
        return $ord_a <=> $ord_b;
    }

    # in the event a custom archive type includes the keyword 'Weekly', etc.
    # order it based on the mapping above.
    unless ($ord_a) {
        if ( ( my $best ) = grep { $a->{archive_type} =~ m/$_/ } keys %order )
        {
            $ord_a = $order{$best};
        }
    }
    unless ($ord_b) {
        if ( ( my $best ) = grep { $b->{archive_type} =~ m/$_/ } keys %order )
        {
            $ord_b = $order{$best};
        }
    }

    # Unknown archive types will follow this pattern (typically)
    #    Foo
    #    Foo-Bar
    # ie, Tag, Tag-Daily, etc.
    # So the archive_type is taken and we'll strip off the second
    # piece, if it matches one of our keys in %order, the sort key
    # becomes Tagnn (where nn is a zero-padded weight, selected from
    # the order hash). Tag-Daily would become Tag02.
    # 'Tag-Rank' would just become TagRank and would sort underneath 'Tag'
    # and other 'Tagnn' keys.
    my $str_a = $a->{archive_type};
    if ( $str_a =~ s/-(.*)$// ) {
        $str_a .= $ord_a ? sprintf( "%02d", $ord_a ) : $1;
    }
    else {
        $str_a = "00" . $str_a if defined($ord_a);
    }

    my $str_b = $b->{archive_type};
    if ( $str_b =~ s/-(.*)$// ) {
        $str_b .= $ord_b ? sprintf( "%02d", $ord_b ) : $1;
    }
    else {
        $str_b = "00" . $str_b if defined($ord_b);
    }
    return $str_a cmp $str_b;
}

sub preview_object_basename {
    my $app = shift;
    my $q   = $app->param;
    my @parts;
    my $blog    = $app->blog;
    my $blog_id = $blog->id if $blog;
    my $id      = $q->param('id');
    push @parts, $app->user->id;
    push @parts, $blog_id || 0;
    push @parts, $id || 0;
    push @parts, $q->param('_type');
    push @parts, $app->config->SecretToken;
    my $data = join ",", @parts;
    return 'mt-preview-' . perl_sha1_digest_hex($data);
}

sub object_edit_uri {
    my $app = shift;
    my ( $type, $id ) = @_;
    my $class = $app->model($type);
    die "no such object $type" unless $class;
    my $obj = $class->load($id)
        or die "object_edit_uri could not find $type object $id";
    my $blog_id = $obj->column('blog_id') if $obj->has_column('blog_id');
    return $app->uri(
        'mode' => 'view',
        args   => {
            '_type' => $type,
            ( $blog_id ? ( blog_id => $blog_id ) : () ), id => $_[1]
        }
    );
}

sub languages_list {
    my $app = shift;
    my ($curr) = @_;

    my $langs = $app->supported_languages;
    my @data;
    $curr ||= $app->config('DefaultLanguage');
    $curr = 'en-us' if ( lc($curr) eq 'en_us' );
    my $curr_lang = $app->current_language;
    for my $tag ( keys %$langs ) {
        ( my $name = $langs->{$tag} ) =~ s/\w+ English/English/;
        $app->set_language($tag);
        my $row = { l_tag => $tag, l_name => $app->translate($name) };
        $row->{l_selected} = 1 if $curr eq $tag;
        push @data, $row;
    }
    $app->set_language($curr_lang);
    [ sort { $a->{l_name} cmp $b->{l_name} } @data ];
}

sub add_to_favorite_blogs {
    my $app = shift;
    my ($fav) = @_;

    my $auth = $app->user;
    my @current = @{ $auth->favorite_blogs || [] };
    return if @current && ( $current[0] == $fav );
    @current = grep { $_ != $fav } @current;
    unshift @current, $fav;
    @current = @current[ 0 .. 9 ]
        if @current > 10;
    $auth->favorite_blogs( \@current );
    $auth->save;
}

sub _entry_prefs_from_params {
    my $app  = shift;
    my $q    = $app->param;
    my $type = $q->param('entry_prefs');
    my %fields;
    if ( $type && lc $type ne 'custom' ) {
        $fields{$type} = 1;
    }
    else {
        $fields{$_} = 1 foreach $q->param('custom_prefs');
    }
    if ( my $body_height = $q->param('text_height') ) {
        $fields{'body'} = $body_height;
    }
    my $prefs = '';
    foreach ( keys %fields ) {
        $prefs .= ',' if $prefs ne '';
        $prefs .= $_;
        $prefs .= ':' . $fields{$_} if $fields{$_} > 1;
    }
    if ( $type && lc $type eq 'custom' ) {
        my @fields = split /,/, $q->param('custom_prefs');
        foreach (@fields) {
            $prefs .= ',' if $prefs ne '';
            $prefs .= $_;
        }
    }
    $prefs .= '|' . $q->param('bar_position');
    $prefs;
}

# rebuild_set is a hash whose keys are entry IDs
# the value can be the entry itself, for efficiency,
# but if the value is not a ref, the entry is loaded from the ID.
# This is not a handler but a utility routine
sub rebuild_these {
    my $app = shift;
    my ( $rebuild_set, %options ) = @_;

    # if there's nothing to rebuild, just return
    if ( !keys %$rebuild_set ) {
        if ( my $start_time = $app->param('start_time') ) {
            $app->publisher->start_time($start_time);
        }

        # now, rebuild indexes for affected blogs
        my @blogs = $app->param('blog_ids');
        if (@blogs) {
            $app->run_callbacks('pre_build') if @blogs;
            foreach my $blog_id (@blogs) {
                my $blog = MT::Blog->load($blog_id) or next;
                $app->rebuild_indexes( Blog => $blog )
                    or return $app->publish_error();
            }
            my $blog_id = int( $app->param('blog_id') );
            my $this_blog = MT::Blog->load($blog_id) if $blog_id;
            $app->run_callbacks( 'rebuild', $this_blog );
            $app->run_callbacks('post_build');
        }
        return $app->call_return;
    }

    if ( exists $options{how} && ( $options{how} eq NEW_PHASE ) ) {
        my $start_time = time;
        $app->run_callbacks('pre_build');
        my $params = {
            return_args => $app->return_args,
            blog_id     => $app->param('blog_id') || 0,
            id          => [ keys %$rebuild_set ],
            start_time  => $start_time,
        };
        my %param = (
            is_full_screen  => 1,
            redirect_target => $app->uri(
                mode => 'rebuild_phase',
                args => $params
            )
        );
        return $app->load_tmpl( 'rebuilding.tmpl', \%param );
    }
    else {
        my @blogs      = $app->param('blog_ids');
        my $start_time = $app->param('start_time');
        $app->publisher->start_time($start_time);
        my %blogs = map { $_ => () } @blogs;
        my @set = keys %$rebuild_set;
        my @rest;
        my $entries_per_rebuild = $app->config('EntriesPerRebuild');
        if ( scalar @set > $entries_per_rebuild ) {
            @rest = @set[ $entries_per_rebuild .. $#set ];
            @set  = @set[ 0 .. $entries_per_rebuild - 1 ];
        }
        require MT::Entry;
        for my $id (@set) {
            my $e = ref $id ? $id : MT::Entry->load($id) or next;
            my $type = $e->class;

            # Rebuilding something that isn't an entry, rebless as required
            #if ( $type ne MT::Entry->class_type ) {
            #    die "had to rebless? $e";
            #    my $pkg = MT->model($type) or next;
            #    bless $e, $pkg;
            #}
            $blogs{ $e->blog_id } = ();
            $app->rebuild_entry(
                Entry             => $e,
                BuildDependencies => 1,
                BuildIndexes      => 0
            ) or return $app->publish_error();
        }
        if (@rest) {
            foreach (@rest) {
                $_ = $_->id if ref $_;
            }
        }
        my $params = {
            return_args     => $app->param('return_args'),
            build_type_name => $app->translate("entry"),
            blog_id         => $app->param('blog_id') || 0,
            blog_ids        => [ keys %blogs ],
            id              => \@rest,
            start_time      => $start_time,
        };
        my %param = (
            is_full_screen  => 1,
            redirect_target => $app->uri(
                mode => 'rebuild_phase',
                args => $params
            )
        );
        return $app->load_tmpl( 'rebuilding.tmpl', \%param );
    }
}

sub remove_preview_file {
    my $app = shift;

    # Clear any preview file that may exist (returning from
    # a preview using the 'reedit', 'cancel' or 'save' buttons)
    if ( my $preview = $app->param('_preview_file') ) {
        require MT::Session;
        if (my $tf = MT::Session->load(
                {   id   => $preview,
                    kind => 'TF',
                }
            )
            )
        {
            my $file = $tf->name;
            my $fmgr = $app->blog->file_mgr;
            $fmgr->delete($file);
            $tf->remove;
        }
    }
}

sub load_text_filters {
    my $app = shift;
    my ( $selected, $type ) = @_;

    $selected = '0' unless defined $selected;

    my $filters = MT->all_text_filters;
    if ( $selected eq '1' ) {
        $selected = '__default__';
    }
    my @f;
    for my $filter ( keys %$filters ) {
        if ( my $c = $filters->{$filter}{condition} ) {
            $c = $app->handler_to_coderef($c) unless ref $c;
            next unless $c->($type);
        }
        my $label = $filters->{$filter}{label};
        $label = $label->() if ref($label) eq 'CODE';
        my $row = {
            key   => $filter,
            label => $label,
        };
        $row->{selected} = $filter eq $selected;
        push @f, $row;
    }
    @f = sort { $a->{label} cmp $b->{label} } @f;
    unshift @f,
        {
        key      => '0',
        label    => $app->translate('None'),
        selected => !$selected,
        };
    return \@f;
}

sub _build_category_list {
    my $app = shift;
    my (%param) = @_;

    my $blog_id         = $param{blog_id};
    my $new_cat_id      = $param{new_cat_id};
    my $include_markers = $param{markers};
    my $counts          = $param{counts};
    my $type            = $param{type};

    my @data;
    my %authors;

    my $class = $app->model($type) or return;

    my %expanded;

    if ($new_cat_id) {
        my $new_cat = $class->load($new_cat_id);
        my @parents = $new_cat->parent_categories;
        %expanded = map { $_->id => 1 } @parents;
    }

    my @cats    = $class->_flattened_category_hierarchy($blog_id);
    my $cols    = $class->column_names;
    my $depth   = 0;
    my $i       = 1;
    my $top_cat = 1;
    my ( $placement_counts, $tb_counts, %tb );

    if ($counts) {
        $app->model('placement');
        $app->model('trackback');
        $app->model('ping');

        my $max_cat_id = 0;
        foreach (@cats) {
            $max_cat_id = $_->id if ( ref $_ ) && ( $_->id > $max_cat_id );
        }

        $placement_counts = {};
        my $cat_entry_count_iter
            = MT::Placement->count_group_by( { blog_id => $blog_id },
            { group => ['category_id'] } );
        while ( my ( $count, $category_id ) = $cat_entry_count_iter->() ) {
            $placement_counts->{$category_id} = $count;
        }

        $tb_counts = {};
        my $tb_count_iter
            = MT::TBPing->count_group_by(
            { blog_id => $blog_id, junk_status => MT::TBPing::NOT_JUNK() },
            { group => ['tb_id'] } );
        while ( my ( $count, $tb_id ) = $tb_count_iter->() ) {
            $tb_counts->{$tb_id} = $count;
        }
        my $tb_iter = MT::Trackback->load_iter(
            {   blog_id     => $blog_id,
                category_id => [ 1, $max_cat_id ]
            },
            { range_incl => { 'category_id' => 1 } }
        );
        while ( my $tb = $tb_iter->() ) {
            $tb{ $tb->category_id } = $tb;
        }
    }

    while ( my $obj = shift @cats ) {
        my $row = {};
        if ( !ref($obj) ) {
            if ( $obj eq 'BEGIN_SUBCATS' ) {
                $depth++;
                $top_cat = 1;
            }
            elsif ( $obj eq 'END_SUBCATS' ) {
                $depth--;
            }
            push @data, { $obj => 1 } if $include_markers;
            next;
        }
        for my $col (@$cols) {
            $row->{ 'category_' . $col } = $obj->$col();
        }
        $row->{category_label} = remove_html( $row->{category_label} );
        if ( $obj->class eq 'folder' ) {
            my $path = $obj->publish_path;
            $row->{category_selector_label} = $path;
            my $path_ids = [];
            foreach my $parent ( $obj->parent_categories ) {
                push @$path_ids, $parent->id;
            }
            $path .= $row->{category_label};
            @$path_ids = reverse @$path_ids;
            $row->{category_path_ids} = $path_ids;

            # $row->{category_label} = $path . '/';
            $row->{category_label_full} 
                = $row->{category_basename} . '/'
                . (
                $obj->label ne $row->{category_basename}
                ? ' <em>' . $obj->label . '</em>'
                : ''
                );
        }
        else {
            my $path     = '';
            my $path_ids = [];
            foreach my $parent ( $obj->parent_categories ) {
                push @$path_ids, $parent->id;
                $path .= $parent->label . ' &#8227; ';
            }
            $path .= $row->{category_label};
            @$path_ids                      = reverse @$path_ids;
            $row->{category_path_ids}       = $path_ids;
            $row->{category_selector_label} = $path;
            $row->{category_label_full}     = $row->{category_label};
        }
        $row->{category_label_spacer} = '&nbsp; ' x $depth;
        if ($counts) {
            $row->{category_entrycount}
                = $placement_counts
                ? ( $placement_counts->{ $obj->id } || 0 )
                : MT::Placement->count( { category_id => $obj->id } );
            if ( my $tb = $tb{ $obj->id } ) {
                $row->{has_tb} = 1;
                $row->{tb_id}  = $tb->id;
                $row->{category_tbcount}
                    = $tb_counts
                    ? ( $tb_counts->{ $tb->id } || 0 )
                    : MT::TBPing->count(
                    {   tb_id       => $tb->id,
                        junk_status => MT::TBPing::NOT_JUNK(),
                    }
                    );
            }
        }
        $row->{category_is_expanded} = 1 if $expanded{ $obj->id };
        $row->{category_pixel_depth} = 10 * $depth;
        $row->{top_cat}              = $top_cat;
        $top_cat                     = 0;
        $row->{is_object}            = 1;
        push @data, $row;
    }
    \@data;
}

sub publish_error {
    my $app = shift;
    my ($msg) = @_;
    if ( defined $app->errstr ) {
        require MT::Log;
        $app->log(
            {   message => (
                    defined $msg ? $msg : $app->translate(
                        "Error during publishing: [_1]",
                        $app->errstr
                    )
                ),
                class    => "system",
                level    => MT::Log::ERROR(),
                category => "publish",
            }
        );
    }
    return undef;
}

1;
__END__

=head1 NAME

MT::App::CMS

=head1 SYNOPSIS

The I<MT::App::CMS> module is the primary application module for
Movable Type. It is the administrative interface that is used to
manage blogs, entries, comments, trackbacks, templates, etc.

=cut
