# Movable Type (r) Open Source (C) 2005-2008 Six Apart, Ltd.
# This program is distributed under the terms of the
# GNU General Public License, version 2.
#
# $Id: CMS.pm 2918 2008-08-11 20:59:04Z bchoate $

package StyleCatcher::CMS;

use strict;
use File::Basename qw(basename);

our $DEFAULT_STYLE_LIBRARY;

sub style_library {
    return MT->registry("stylecatcher_libraries");
}

sub file_mgr {
    my $app = MT->instance;
    require MT::FileMgr;
    my $filemgr = MT::FileMgr->new('Local')
      or return $app->error( MT::FileMgr->errstr );
    $filemgr;
}

sub listify {
    my ($data) = @_;
    my @list;
    foreach my $k (keys %$data) {
        my %entry = %{ $data->{$k} };
        $entry{key} = $k;
        delete $entry{plugin};
        $entry{label} = $entry{label}->() if ref($entry{label});
        $entry{description_label} = $entry{description_label}->() if ref($entry{description_label});
        push @list, \%entry;
    }
    @list = sort { $a->{order} <=> $b->{order} } @list;
    \@list;
}

sub view {
    my $app     = shift;
    my $blog_id = $app->param('blog_id');
    $app->return_to_dashboard( redirect => 1 ) unless $blog_id;

    my $blog = MT::Blog->load($blog_id);
    return $app->errtrans("Invalid request") unless $blog;

    my $static_path = $app->static_file_path;
    if (! -d $static_path ) {
        return $app->errtrans("Your mt-static directory could not be found. Please configure 'StaticFilePath' to continue.");
    }

    my $themeroot =
      File::Spec->catdir( $app->static_file_path, 'support', 'themes' );
    my $webthemeroot = $app->static_path . 'support/themes';
    my $stylelibrary = listify(style_library());
    my $theme_data   = make_themes();
    my $styled_blogs = fetch_blogs();

    my $config = plugin()->get_config_hash();

    my @blog_loop;
    my %current_themes;
    my ($blog_theme, $blog_layout);
    foreach my $blog (@$styled_blogs) {
        my $curr_theme = $config->{"current_theme_" . $blog->id} || '';
        my $curr_layout = $config->{"current_layout_" . $blog->id} || 'layout-wtt';
        push @blog_loop,
          {
            blog_id   => $blog->id,
            blog_name => $blog->name,
            layout    => $curr_layout,
            theme_id  => $curr_theme,
            view_link => $blog->site_url,
          };
        if ($blog->id == $blog_id) {
            $blog_theme = $curr_theme;
            $blog_layout = $curr_layout;
        }
        if ( $theme_data->{themes} && $curr_theme ) {
            foreach my $theme ( @{ $theme_data->{themes} } ) {
                if ( ($theme->{prefix} || '') . ':' . $theme->{name} eq $curr_theme ) {
                    push @{ $theme->{blogs} }, $blog->id;
                    next if exists $current_themes{ $theme->{name} };
                    $current_themes{ $theme->{name} } = 1;
                    push @{ $theme->{tags} }, 'collection:current';
                }
            }
        }
    }

    push @{ $theme_data->{categories} }, 'current'
      if %current_themes;

    require JSON;
    my $url   = $app->param('url');
    my %param = (
        version     => plugin()->version,
        # blog_loop   => \@blog_loop,
        blog_id => $blog_id,
        themes_json => JSON::objToJson(
            $theme_data, { pretty => 1, indent => 2, delimiter => 1 }
        ),
        auto_fetch => $url ? 1 : 0,
        style_library => $stylelibrary,
        current_theme => $blog_theme || '',
        current_layout => $blog_layout || 'layout-wtt',
        dynamic_blog => (($blog->custom_dynamic_templates || '') eq 'all'),
    );

    if ( $blog_id && @$styled_blogs ) {
        my $blog = $styled_blogs->[0];
        $param{blog_name} = $blog->name;
        $param{blog_url}  = $blog->site_url;
    }

    my $path = $app->static_path;
    $path .= '/' unless $path =~ m!/$!;
    $path .= plugin()->envelope . "/";
    $path = $app->base . $path if $path =~ m!^/!;
    $param{plugin_static_uri} = $path;

    $app->build_page( 'view.tmpl', \%param );
}

# AJAX/JSON modes

# returns a json structure of styles given a particular url
sub js {
    # ydnar's remixer uses javascript files for each collection of styles -
    # we generate these js files from css metadata
    # StyleCatcher will pick up any metadata in the theme css file in the
    # format of 'key: value' in comment-space
    # The remixer only uses name, author, description at the moment.
    my $app = shift;
    return $app->json_error( $app->errstr ) unless $app->validate_magic;

    my $data = fetch_themes($app->param('url'))
        or return $app->json_error( $app->errstr );
    return $app->json_result( $data );
}

# does the work after user selects a particular theme to apply to a blog
sub apply {
    my $app = shift;

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

    # Load the default stylesheet for this blog
    my $tmpl = load_style_template($blog_id);

    $app->validate_magic or return $app->json_error($app->translate("Invalid request"));
    return $app->json_error($app->translate("Invalid request"))
      unless $blog_id && $url && $tmpl;

    my $static_path = $app->static_file_path;
    if (! -d $static_path ) {
        return $app->json_error($app->translate("Your mt-static directory could not be found. Please configure 'StaticFilePath' to continue."));
    }

    my $themeroot =
      File::Spec->catdir( $static_path, 'support', 'themes' );
    my $webthemeroot = $app->static_path . 'support/themes/';
    my $mtthemeroot  = $app->static_path . 'themes/';
    my $mtthemebase  = $app->static_path . 'themes-base/';

    # Break up the css url in to a couple useful pieces
    my @url = split( /\//, $url );

    # if this isn't a local url, then we have to grab some files from
    # yonder...
    my $filemgr = file_mgr()
      or return $app->json_error( MT::FileMgr->errstr );

    if ( $url !~ m/^(\Q$webthemeroot\E|\Q$mtthemeroot\E)/ ) {
        my $new_url = '';

        for (0..(scalar(@url)-2)) {
            $new_url .= $url[$_] . '/';
        }
        my ( $basename, $extension ) = split( /\./, $url[-1] );
        if ($basename eq 'screen') {
            $basename = $url[-2];
        }

        # Pick up the stylesheet
        my $user_agent  = $app->new_ua;
        my $css_request = HTTP::Request->new( GET => $url );
        my $response    = $user_agent->request($css_request);

        # Pick up the thumbnail and thumbnail-large
        my $thumbnail_request =
          HTTP::Request->new( GET => $new_url . "thumbnail.gif" );
        my $thumbnail_response = $user_agent->request($thumbnail_request);
        my $thumbnail_large_request =
          HTTP::Request->new( GET => $new_url . "thumbnail-large.gif" );
        my $thumbnail_large_response =
          $user_agent->request($thumbnail_large_request);

        # Parse out image filenames in the css and then write out the css file
        # and thumbnails to our theme folder
        my $content = $response->content;
        $content =~ s!/\*.*?\*/!!gs;    # strip all comments first
        my @images = $content =~
          m/\b(?:url\(\s*)([a-zA-Z0-9_.-]+\.(?:gif|jpe?g|png))(?:\s*?\))/gi;
        $filemgr->mkpath( File::Spec->catdir( $themeroot, $basename ) )
          or return $app->json_error(
            $app->translate(
"Could not create [_1] folder - Check that your 'themes' folder is webserver-writable.",
                $basename
            )
          );
        $filemgr->put_data( $response->content,
            File::Spec->catfile( $themeroot, $basename, $basename . '.css' ) );
        if (($thumbnail_response->code >= 200) && ($thumbnail_response->code < 400)) {
        $filemgr->put_data( $thumbnail_response->content,
            File::Spec->catfile( $themeroot, $basename, "thumbnail.gif" ),
            'upload' );
        } else {
            return $app->json_error($app->translate("Error downloading image: [_1]", $new_url . 'thumbnail.gif'))
        }
        if (($thumbnail_large_response->code >= 200) && ($thumbnail_large_response->code < 400)) {
            $filemgr->put_data(
                $thumbnail_large_response->content,
                File::Spec->catfile( $themeroot, $basename, "thumbnail-large.gif" ),
                'upload'
            );
        } else {
            return $app->json_error($app->translate("Error downloading image: [_1]", $new_url . 'thumbnail-large.gif'))
        }

       # Pick up the images we parsed earlier and write them to the theme folder
        for my $image_url (@images) {
            my $image_request =
              HTTP::Request->new( GET => $new_url . $image_url );
            my $image_response = $user_agent->request($image_request);

            my @image_url = split( /\//, $image_url );
            my $image_filename = $image_url[-1];

            if (($response->code >= 200) && ($response->code < 400)) {
                $filemgr->put_data( $image_response->content,
                    File::Spec->catfile( $themeroot, $basename, $image_filename ),
                    'upload' )
                  or return $app->json_error( $filemgr->errstr );
            } else {
                return $app->json_error($app->translate("Error downloading image: [_1]", $new_url . $image_url));
            }
        }
        $url = "$webthemeroot$basename/$basename.css";
    }
    
    my $blog = MT->model('blog')->load($blog_id)    
      or return $app->json_error( $app->translate('No such blog [_1]', $blog_id) );

    my $r = MT->registry;
    my $base_css = $r->{"template_sets"}{$blog->template_set}{"base_css"};

    # Replacing the theme import or adding a new one at the beginning
    my $template_text  = $tmpl->text();
    my $replaced       = 0;
    my $header =
'/* This is the StyleCatcher theme addition. Do not remove this block. */';
    my $footer = '/* end StyleCatcher imports */';
    my $styles = $header . "\n";
    $styles .= "\@import url(".MT::Util::caturl($app->static_path, $base_css).");\n" if $base_css;
    $styles .= "\@import url($url);\n";
    $styles .= $footer;
    print STDERR "styles=$styles\n";

    if ($template_text =~ s/\Q$header\E.*\Q$footer\E/$styles/s) {
        $tmpl->text( $template_text );
        $replaced = 1;
    }
    unless ($replaced) {

        # we're dealing with a template that wasn't modified before now
        # we will need to backup the existing one to make sure the new
        # style is applied properly.
        my @ts = MT::Util::offset_time_list( time, $blog_id );
        my $ts = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $ts[5] + 1900,
          $ts[4] + 1, @ts[ 3, 2, 1, 0 ];
        my $backup = $tmpl->clone;
        delete $backup->{column_values}
          {id};    # make sure we don't overwrite original
        delete $backup->{changed_cols}{id};
        $backup->name( $backup->name . ' (Backup from ' . $ts . ')' );
        $backup->outfile('');
        $backup->linked_file( $tmpl->linked_file );
        $backup->rebuild_me(0);
        $backup->build_dynamic(0);
        $backup->identifier(undef);
        $backup->type('backup');
        $backup->save;
        $tmpl->linked_file('');    # make sure this one isn't linked now
        $tmpl->identifier('styles');
        $tmpl->text($styles);
    }

    # Putting the stylesheet back together again
    $tmpl->save or return $app->json_error( $tmpl->errstr );

    $blog->page_layout($layout);
    $blog->touch();
    $blog->save();

    # rebuild only the stylesheet! forcibly. with prejudice.
    $app->rebuild_indexes(
        BlogID   => $tmpl->blog_id,
        Template => $tmpl,
        Force    => 1
    );

    my $p = plugin();
    $name =~ s/^repo_\d+:/local:/;
    $name =~ s/\.css$//;
    $p->set_config_value('current_theme_' . $blog_id, $name);
    if ($layout) {
        $p->set_config_value('current_layout_' . $blog_id, $layout);
    } else {
        $p->set_config_value('current_layout_' . $blog_id, undef);
    }

    return $app->json_result(
        {
            message =>
              $app->translate("Successfully applied new theme selection.")
        }
    );
}

# Utility methods

sub fetch_blogs {
    my $app     = MT->app;
    my $user    = $app->user;
    my $blog_id = $app->param('blog_id');

    my @blogs;
    if ($blog_id) {
        @blogs = MT::Blog->load($blog_id);
    } else {
        if ( $user->is_superuser() ) {
            if ($blog_id) {
                @blogs = MT::Blog->load($blog_id);
            }
        }
        else {
            my $args = { author_id => $user->id };
            $args->{blog_id} = $blog_id if $blog_id;
            require MT::Permission;
            my @perms = MT::Permission->load( { author_id => $user->id } );
            foreach my $perm (@perms) {
                next unless $perm->can_edit_templates;
                push @blogs, MT::Blog->load( $perm->blog_id );
            }
        }
    }
    my @styled_blogs;
    foreach my $blog (@blogs) {
        my $tmpl = load_style_template( $blog->id );
        if ($tmpl) {
            push @styled_blogs, $blog;
        }
    }
    @styled_blogs = sort { $a->name cmp $b->name } @styled_blogs;

    \@styled_blogs;
}

sub load_style_template {
    my ($blog_id) = @_;

    require MT::Template;
    my $tmpl;

    $tmpl = MT::Template->load(
        {
            blog_id    => $blog_id,
            identifier => 'styles'
        }
    );

    $tmpl ||= MT::Template->load(
        {
            blog_id => $blog_id,
            outfile => "styles.css"
        }
    );

    # MT 3.x era stylesheet file
    $tmpl ||= MT::Template->load(
        {
            blog_id => $blog_id,
            outfile => "styles-site.css"
        }
    );

    unless ($tmpl) {

        # Create one since we didn't find a candidate
        $tmpl = new MT::Template;
        $tmpl->blog_id($blog_id);
        $tmpl->identifier('styles');
        $tmpl->outfile("styles.css");
        $tmpl->text(<<'EOT');
@import url(<$MTStaticWebPath$>themes-base/blog.css);
@import url(<$MTStaticWebPath$>themes/minimalist-red/styles.css);
EOT
        $tmpl->save();
    }

    $tmpl;
}

# pulls a list of themes available from a particular url
sub fetch_themes {
    my $app = MT->app;
    my ($url) = @_;
    return undef unless $url;

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

  # If we have a url then we're specifying a specific theme (css) or repo (html)
    # Pick up the file (html with <link>s or a css file with metadata)
    my $user_agent = $app->new_ua;
    my $request    = HTTP::Request->new( GET => $url );
    my $response   = $user_agent->request($request);

    # Make a repo if you've got a ton of links or an automagic entry if
    # you're a css file
    my $type = $response->headers->{'content-type'};
    $type = shift @$type if ref $type eq 'ARRAY';
    if ( $type =~ m!^text/css! ) {
        $data->{auto}{url} = $url;
        my $theme = fetch_theme( $url, ['collection:auto'] );
        $data->{themes} = [$theme];
    }
    elsif ( $type =~ m!^text/html! ) {
        my @repo_themes;
        for my $link (
            ref( $response->headers->{'link'} ) eq 'ARRAY'
            ? @{ $response->headers->{'link'} }
            : $response->headers->{'link'}
          )
        {
            my ( $css, @parsed_link ) = split( /;/, $link );
            $css =~ s/[<>]//g;
            my %attr;
            foreach (@parsed_link) {
                my ( $name, $val ) = split /=/, $_, 2;
                $name =~ s/^ //;
                $val  =~ s/^['"]|['"]$//g;
                next if $name eq '/';
                $attr{ lc($name) } = $val;
            }
            next unless lc $attr{rel}  eq 'theme';
            next unless lc $attr{type} eq 'text/x-theme';

            # Fix for relative theme locations
            if ($css !~ m!^https?://!) {
                my $new_css = $url;
                $new_css =~ s!/[a-z0-9_-]+\.[a-z]+?$|/$!/!;
                $new_css .= $css;
                $css = $new_css;
            }
            push @repo_themes, $css;
        }

        my $themes = [];
        for my $repo_theme (@repo_themes) {
            my $theme = fetch_theme( $repo_theme, [] );
            push @$themes, $theme if $theme;
        }
        $data->{themes} = $themes;
        if ( $data->{repo}{display_name} = $response->headers->{'title'} ) {
            $data->{repo}{name} =
              MT::Util::dirify( $data->{repo}{display_name} );
        }
        else {
            $data->{repo}{display_name} = $url;
            $data->{repo}{name}         = MT::Util::dirify($url);
        }
        $data->{repo}{url} = $url;
    }
    else {
        return $app->error( $app->translate('Invalid URL: [_1]', $url) );
    }

    $data;
}

# sets up the object structure we return through json to populate
# the mixer.
sub make_themes {
    my $app = MT->instance;

    # categories
    #   current    (for active theme)
    #   repo       (for themes found at repo link)
    #   my-designs (for themes that are stored locally)
    #   mt-designs (for themes that are local and installed by default)
    #   auto       (for link to a single css file)

    # structure of "data"
    #   categories => [ one, two, three ]  ie: 'current', 'repo'
    #   themes => [
    #       { theme }
    #   ]
    #   repo => {
    #       display_name => 'display name',
    #       name => 'repo name',
    #       url => 'url of repo',
    #   }

# structure of "theme"
#   theme => {
#       name => 'theme_dir',
#       imageSmall => 'link_to/thumbnail.gif',
#       imageBig => 'link_to/thumbnail-large.gif',
#       title => 'Theme Title',
#       description => 'Theme description.',
#       url_css => 'link_to/theme.css',
#       url_zip => 'link_to/theme.zip',
#       author => 'Author Name',
#       author_url => 'http://author.com/'
#       author_affiliation => 'Author Co.',
#       layouts => "comma,delimited,layout,list"
#       sort => 'theme_sortable_name',
#       tags => ['association:tag']  ie, 'color:blue', 'designer:author', 'collection:repo'
#   }

    my ( $categories, $themes );
    my $sys_root = File::Spec->catdir( $app->static_file_path, 'themes' );

    # Generate our list of themes within the themeroot directory
    my @sys_list = glob( File::Spec->catfile( $sys_root, "*" ) );
    $categories->{'mt-designs'} = 1 if @sys_list;
    for my $theme (@sys_list) {
        my $theme_dir = $theme;
        my $theme_url = $app->static_path . 'themes';
        next unless -d $theme;
        $theme =~ s/.*[\\\/]//;
        $themes->{$theme} =
          fetch_theme( $theme_dir, ['collection:mt-designs'], $theme_url,
            $theme_dir );
        $themes->{$theme}{name} = $themes->{$theme}{name};
        $themes->{$theme}{prefix} = 'default';
    }

    my $themeroot =
      File::Spec->catdir( $app->static_file_path, 'support', 'themes' );

    # Generate our list of themes within the themeroot directory
    my @themeroot_list = glob( File::Spec->catfile( $themeroot, "*" ) );
    $categories->{'my-designs'} = 1 if @themeroot_list;
    for my $theme (@themeroot_list) {
        my $theme_dir = $theme;
        next unless -d $theme;
        $theme =~ s/.*[\\\/]//;
        $themes->{$theme} =
          fetch_theme( $theme_dir, ['collection:my-designs'] );
        $themes->{$theme}{prefix} = 'local';
    }

    my $data = {
        categories => [ keys %$categories ],
        themes     => [ values %$themes ]
    };

    $data;
}

sub fetch_theme {
    my $app = MT->app;
    my ( $url, $tags, $baseurl, $basepath ) = @_;

    my $theme;
    my $stylesheet;
    my $new_url;
    my $themeroot;
    if ( $url =~ m/^https?:/i ) {

        # Pick up the css file
        my $user_agent  = $app->new_ua;
        my $css_request = HTTP::Request->new( GET => $url );
        my $response    = $user_agent->request($css_request);
        $stylesheet = $response->content if ($response->code >= 200) && ($response->code < 400);
        return unless $stylesheet;

# Break up the css url in to a couple useful pieces (generalize and break me out)
        $theme = $url;
        # discard any generic 'screen.css' filename
        $theme =~ s!/screen.css$!!;
        $theme =~ s/.*[\\\/]//;
        my @url = split( /\//, $url );
        for ( 0 .. ( scalar(@url) - 2 ) ) {
            $new_url .= $url[$_] . '/';
        }
    }
    else {
        $themeroot = $basepath
          || File::Spec->catdir( $app->static_file_path, 'support', 'themes' );
        my $webthemeroot = $baseurl || $app->static_path . 'support/themes';

        $theme = $url;
        $theme =~ s/.*[\\\/]//;
        my $file = File::Spec->catfile( $url, "$theme.css" );
        $new_url = "$webthemeroot/$theme/";
        if ( -e $file ) {
            $stylesheet = file_mgr()->get_data($file);
            $url        = $new_url . "$theme.css";
        }
        else {
            $file = File::Spec->catfile( $url, "screen.css" );
            if ( -e $file ) {
                $stylesheet = file_mgr()->get_data($file);
                $url        = $new_url . "screen.css";
            }
        }
    }

    # Pick up the metadata from the css
    my @css_lines = split( /\r?\n/, $stylesheet || '' );
    my $commented = 0;
    my @comments;
    for my $line (@css_lines) {
        my $pos;
        $pos = index( $line, "/*" );
        unless ( $pos == -1 ) {
            $line = substr( $line, $pos + 2 );
            $commented = 1;
        }
        if ($commented) {
            $pos = index( $line, "*/" );
            unless ( $pos == -1 ) {
                $line = substr( $line, 0, $pos );
                $commented = 0;
            }
            push @comments, $line;
        }
    }

    my $comment;
    my %metadata;

    # Trim me white space, yarr
    for (@comments) {

        # TBD: strip any "risky" content; we don't want any
        # XSS in this content.
        # Strip any null bytes
        tr/\x00//d;
        s/^\s+|\s+$//g;
        my ( $key, $value ) = split( /:/, $_, 2 ) or next;
        next unless defined $value;
        $value =~ s/^\s+//;
        $metadata{ lc $key } = $value;
    }

    my $thumbnail_link;
    $thumbnail_link = $new_url . 'thumbnail.gif';
    my $thumbnail_large_link;
    $thumbnail_large_link = $new_url . 'thumbnail-large.gif';

    require MT::Util;
    my $data = {
        name        => $theme,
        description => $metadata{description} || '',
        title       => $metadata{name} || '(Untitled)',
        url         => $url,
        imageSmall  => $thumbnail_link,
        imageBig    => $thumbnail_large_link,
        author      => $metadata{designer} || $metadata{author} || '',
        author_url  => $metadata{designer_url} || $metadata{author_url} || '',
        author_affiliation => $metadata{author_affiliation} || '',
        layouts            => $metadata{layouts} || '',
        'sort'             => $metadata{name} || '',
        tags               => $tags,
        blogs              => [],
    };
    $data;
}

sub plugin {
    return MT->component('StyleCatcher');
}

1;
