root/trunk/StyleCatcher/plugins/StyleCatcher/lib/StyleCatcher/CMS.pm @ 817

Revision 817, 22.4 kB (checked in by breese, 17 months ago)

bringing the version of StyleCatcher? in the mtplugins repos up to date with what (will) ships with Movable Type 4.2

Line 
1# Movable Type (r) Open Source (C) 2005-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id: CMS.pm 2576 2008-06-14 00:35:33Z bchoate $
6
7package StyleCatcher::CMS;
8
9use strict;
10use File::Basename qw(basename);
11
12our $DEFAULT_STYLE_LIBRARY;
13
14sub style_library {
15    return MT->registry("stylecatcher_libraries");
16}
17
18sub file_mgr {
19    my $app = MT->instance;
20    require MT::FileMgr;
21    my $filemgr = MT::FileMgr->new('Local')
22      or return $app->error( MT::FileMgr->errstr );
23    $filemgr;
24}
25
26sub listify {
27    my ($data) = @_;
28    my @list;
29    foreach my $k (keys %$data) {
30        my %entry = %{ $data->{$k} };
31        $entry{key} = $k;
32        delete $entry{plugin};
33        $entry{label} = $entry{label}->() if ref($entry{label});
34        $entry{description_label} = $entry{description_label}->() if ref($entry{description_label});
35        push @list, \%entry;
36    }
37    @list = sort { $a->{order} <=> $b->{order} } @list;
38    \@list;
39}
40
41sub view {
42    my $app     = shift;
43    my $blog_id = $app->param('blog_id');
44    $app->return_to_dashboard( redirect => 1 ) unless $blog_id;
45
46    my $blog = MT::Blog->load($blog_id);
47    return $app->errtrans("Invalid request") unless $blog;
48
49    my $static_path = $app->static_file_path;
50    if (! -d $static_path ) {
51        return $app->errtrans("Your mt-static directory could not be found. Please configure 'StaticFilePath' to continue.");
52    }
53
54    my $themeroot =
55      File::Spec->catdir( $app->static_file_path, 'support', 'themes' );
56    my $webthemeroot = $app->static_path . 'support/themes';
57    my $stylelibrary = listify(style_library());
58    my $theme_data   = make_themes();
59    my $styled_blogs = fetch_blogs();
60
61    my $config = plugin()->get_config_hash();
62
63    my @blog_loop;
64    my %current_themes;
65    my ($blog_theme, $blog_layout);
66    foreach my $blog (@$styled_blogs) {
67        my $curr_theme = $config->{"current_theme_" . $blog->id} || '';
68        my $curr_layout = $config->{"current_layout_" . $blog->id} || 'layout-wtt';
69        push @blog_loop,
70          {
71            blog_id   => $blog->id,
72            blog_name => $blog->name,
73            layout    => $curr_layout,
74            theme_id  => $curr_theme,
75            view_link => $blog->site_url,
76          };
77        if ($blog->id == $blog_id) {
78            $blog_theme = $curr_theme;
79            $blog_layout = $curr_layout;
80        }
81        if ( $theme_data->{themes} && $curr_theme ) {
82            foreach my $theme ( @{ $theme_data->{themes} } ) {
83                if ( ($theme->{prefix} || '') . ':' . $theme->{name} eq $curr_theme ) {
84                    push @{ $theme->{blogs} }, $blog->id;
85                    next if exists $current_themes{ $theme->{name} };
86                    $current_themes{ $theme->{name} } = 1;
87                    push @{ $theme->{tags} }, 'collection:current';
88                }
89            }
90        }
91    }
92
93    push @{ $theme_data->{categories} }, 'current'
94      if %current_themes;
95
96    require JSON;
97    my $url   = $app->param('url');
98    my %param = (
99        version     => plugin()->version,
100        # blog_loop   => \@blog_loop,
101        blog_id => $blog_id,
102        themes_json => JSON::objToJson(
103            $theme_data, { pretty => 1, indent => 2, delimiter => 1 }
104        ),
105        auto_fetch => $url ? 1 : 0,
106        style_library => $stylelibrary,
107        current_theme => $blog_theme || '',
108        current_layout => $blog_layout || 'layout-wtt',
109        dynamic_blog => (($blog->custom_dynamic_templates || '') eq 'all'),
110    );
111
112    if ( $blog_id && @$styled_blogs ) {
113        my $blog = $styled_blogs->[0];
114        $param{blog_name} = $blog->name;
115        $param{blog_url}  = $blog->site_url;
116    }
117
118    my $path = $app->static_path;
119    $path .= '/' unless $path =~ m!/$!;
120    $path .= plugin()->envelope . "/";
121    $path = $app->base . $path if $path =~ m!^/!;
122    $param{plugin_static_uri} = $path;
123
124    $app->build_page( 'view.tmpl', \%param );
125}
126
127# AJAX/JSON modes
128
129# returns a json structure of styles given a particular url
130sub js {
131    # ydnar's remixer uses javascript files for each collection of styles -
132    # we generate these js files from css metadata
133    # StyleCatcher will pick up any metadata in the theme css file in the
134    # format of 'key: value' in comment-space
135    # The remixer only uses name, author, description at the moment.
136    my $app = shift;
137
138    my $data = fetch_themes($app->param('url'))
139        or return $app->json_error( $app->errstr );
140    return $app->json_result( $data );
141}
142
143# does the work after user selects a particular theme to apply to a blog
144sub apply {
145    my $app = shift;
146
147    my $blog_id = $app->param('blog_id');
148    my $url     = $app->param('url');
149    my $layout  = $app->param('layout');
150    my $name    = $app->param('name');
151
152    # Load the default stylesheet for this blog
153    my $tmpl = load_style_template($blog_id);
154
155    $app->validate_magic or return $app->json_error($app->translate("Invalid request"));
156    return $app->json_error($app->translate("Invalid request"))
157      unless $blog_id && $url && $tmpl;
158
159    my $static_path = $app->static_file_path;
160    if (! -d $static_path ) {
161        return $app->json_error($app->translate("Your mt-static directory could not be found. Please configure 'StaticFilePath' to continue."));
162    }
163
164    my $themeroot =
165      File::Spec->catdir( $static_path, 'support', 'themes' );
166    my $webthemeroot = $app->static_path . 'support/themes/';
167    my $mtthemeroot  = $app->static_path . 'themes/';
168    my $mtthemebase  = $app->static_path . 'themes-base/';
169
170    # Break up the css url in to a couple useful pieces
171    my @url = split( /\//, $url );
172
173    # if this isn't a local url, then we have to grab some files from
174    # yonder...
175    my $filemgr = file_mgr()
176      or return $app->json_error( MT::FileMgr->errstr );
177
178    if ( $url !~ m/^(\Q$webthemeroot\E|\Q$mtthemeroot\E)/ ) {
179        my $new_url = '';
180
181        for (0..(scalar(@url)-2)) {
182            $new_url .= $url[$_] . '/';
183        }
184        my ( $basename, $extension ) = split( /\./, $url[-1] );
185        if ($basename eq 'screen') {
186            $basename = $url[-2];
187        }
188
189        # Pick up the stylesheet
190        my $user_agent  = $app->new_ua;
191        my $css_request = HTTP::Request->new( GET => $url );
192        my $response    = $user_agent->request($css_request);
193
194        # Pick up the thumbnail and thumbnail-large
195        my $thumbnail_request =
196          HTTP::Request->new( GET => $new_url . "thumbnail.gif" );
197        my $thumbnail_response = $user_agent->request($thumbnail_request);
198        my $thumbnail_large_request =
199          HTTP::Request->new( GET => $new_url . "thumbnail-large.gif" );
200        my $thumbnail_large_response =
201          $user_agent->request($thumbnail_large_request);
202
203        # Parse out image filenames in the css and then write out the css file
204        # and thumbnails to our theme folder
205        my $content = $response->content;
206        $content =~ s!/\*.*?\*/!!gs;    # strip all comments first
207        my @images = $content =~
208          m/\b(?:url\(\s*)([a-zA-Z0-9_.-]+\.(?:gif|jpe?g|png))(?:\s*?\))/gi;
209        $filemgr->mkpath( File::Spec->catdir( $themeroot, $basename ) )
210          or return $app->json_error(
211            $app->translate(
212"Could not create [_1] folder - Check that your 'themes' folder is webserver-writable.",
213                $basename
214            )
215          );
216        $filemgr->put_data( $response->content,
217            File::Spec->catfile( $themeroot, $basename, $basename . '.css' ) );
218        if (($thumbnail_response->code >= 200) && ($thumbnail_response->code < 400)) {
219        $filemgr->put_data( $thumbnail_response->content,
220            File::Spec->catfile( $themeroot, $basename, "thumbnail.gif" ),
221            'upload' );
222        } else {
223            return $app->json_error($app->translate("Error downloading image: [_1]", $new_url . 'thumbnail.gif'))
224        }
225        if (($thumbnail_large_response->code >= 200) && ($thumbnail_large_response->code < 400)) {
226            $filemgr->put_data(
227                $thumbnail_large_response->content,
228                File::Spec->catfile( $themeroot, $basename, "thumbnail-large.gif" ),
229                'upload'
230            );
231        } else {
232            return $app->json_error($app->translate("Error downloading image: [_1]", $new_url . 'thumbnail-large.gif'))
233        }
234
235       # Pick up the images we parsed earlier and write them to the theme folder
236        for my $image_url (@images) {
237            my $image_request =
238              HTTP::Request->new( GET => $new_url . $image_url );
239            my $image_response = $user_agent->request($image_request);
240
241            my @image_url = split( /\//, $image_url );
242            my $image_filename = $image_url[-1];
243
244            if (($response->code >= 200) && ($response->code < 400)) {
245                $filemgr->put_data( $image_response->content,
246                    File::Spec->catfile( $themeroot, $basename, $image_filename ),
247                    'upload' )
248                  or return $app->json_error( $filemgr->errstr );
249            } else {
250                return $app->json_error($app->translate("Error downloading image: [_1]", $new_url . $image_url));
251            }
252        }
253        $url = "$webthemeroot$basename/$basename.css";
254    }
255   
256
257    my $url2 = $mtthemebase . "blog.css";
258
259    # Replacing the theme import or adding a new one at the beginning
260    my $template_text  = $tmpl->text();
261    my $replaced       = 0;
262    my $header =
263'/* This is the StyleCatcher theme addition. Do not remove this block. */';
264    my $footer = '/* end StyleCatcher imports */';
265    my $styles = $header . "\n" . <<"EOT" . $footer;
266\@import url($url2);
267\@import url($url);
268EOT
269    if ($template_text =~ s/\Q$header\E.*\Q$footer\E/$styles/s) {
270        $tmpl->text( $template_text );
271        $replaced = 1;
272    }
273    unless ($replaced) {
274
275        # we're dealing with a template that wasn't modified before now
276        # we will need to backup the existing one to make sure the new
277        # style is applied properly.
278        my @ts = MT::Util::offset_time_list( time, $blog_id );
279        my $ts = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $ts[5] + 1900,
280          $ts[4] + 1, @ts[ 3, 2, 1, 0 ];
281        my $backup = $tmpl->clone;
282        delete $backup->{column_values}
283          {id};    # make sure we don't overwrite original
284        delete $backup->{changed_cols}{id};
285        $backup->name( $backup->name . ' (Backup from ' . $ts . ')' );
286        $backup->outfile('');
287        $backup->linked_file( $tmpl->linked_file );
288        $backup->rebuild_me(0);
289        $backup->build_dynamic(0);
290        $backup->identifier(undef);
291        $backup->type('backup');
292        $backup->save;
293        $tmpl->linked_file('');    # make sure this one isn't linked now
294        $tmpl->identifier('styles');
295        $tmpl->text($styles);
296    }
297
298    # Putting the stylesheet back together again
299    $tmpl->save or return $app->json_error( $tmpl->errstr );
300
301    my $blog = MT->model('blog')->load($blog_id)   
302      or return $app->json_error( $app->translate('No such blog [_1]', $blog_id) );
303    $blog->page_layout($layout);
304    $blog->touch();
305    $blog->save();
306
307    # rebuild only the stylesheet! forcibly. with prejudice.
308    $app->rebuild_indexes(
309        BlogID   => $tmpl->blog_id,
310        Template => $tmpl,
311        Force    => 1
312    );
313
314    my $p = plugin();
315    $name =~ s/^repo_\d+:/local:/;
316    $name =~ s/\.css$//;
317    $p->set_config_value('current_theme_' . $blog_id, $name);
318    if ($layout) {
319        $p->set_config_value('current_layout_' . $blog_id, $layout);
320    } else {
321        $p->set_config_value('current_layout_' . $blog_id, undef);
322    }
323
324    return $app->json_result(
325        {
326            message =>
327              $app->translate("Successfully applied new theme selection.")
328        }
329    );
330}
331
332# Utility methods
333
334sub fetch_blogs {
335    my $app     = MT->app;
336    my $user    = $app->user;
337    my $blog_id = $app->param('blog_id');
338
339    my @blogs;
340    if ($blog_id) {
341        @blogs = MT::Blog->load($blog_id);
342    } else {
343        if ( $user->is_superuser() ) {
344            if ($blog_id) {
345                @blogs = MT::Blog->load($blog_id);
346            }
347        }
348        else {
349            my $args = { author_id => $user->id };
350            $args->{blog_id} = $blog_id if $blog_id;
351            require MT::Permission;
352            my @perms = MT::Permission->load( { author_id => $user->id } );
353            foreach my $perm (@perms) {
354                next unless $perm->can_edit_templates;
355                push @blogs, MT::Blog->load( $perm->blog_id );
356            }
357        }
358    }
359    my @styled_blogs;
360    foreach my $blog (@blogs) {
361        my $tmpl = load_style_template( $blog->id );
362        if ($tmpl) {
363            push @styled_blogs, $blog;
364        }
365    }
366    @styled_blogs = sort { $a->name cmp $b->name } @styled_blogs;
367
368    \@styled_blogs;
369}
370
371sub load_style_template {
372    my ($blog_id) = @_;
373
374    require MT::Template;
375    my $tmpl;
376
377    $tmpl = MT::Template->load(
378        {
379            blog_id    => $blog_id,
380            identifier => 'styles'
381        }
382    );
383
384    $tmpl ||= MT::Template->load(
385        {
386            blog_id => $blog_id,
387            outfile => "styles.css"
388        }
389    );
390
391    # MT 3.x era stylesheet file
392    $tmpl ||= MT::Template->load(
393        {
394            blog_id => $blog_id,
395            outfile => "styles-site.css"
396        }
397    );
398
399    unless ($tmpl) {
400
401        # Create one since we didn't find a candidate
402        $tmpl = new MT::Template;
403        $tmpl->blog_id($blog_id);
404        $tmpl->identifier('styles');
405        $tmpl->outfile("styles.css");
406        $tmpl->text(<<'EOT');
407@import url(<$MTStaticWebPath$>themes-base/blog.css);
408@import url(<$MTStaticWebPath$>themes/minimalist-red/styles.css);
409EOT
410        $tmpl->save();
411    }
412
413    $tmpl;
414}
415
416# pulls a list of themes available from a particular url
417sub fetch_themes {
418    my $app = MT->app;
419    my ($url) = @_;
420    return undef unless $url;
421
422    my $blog_id = $app->param('blog_id');
423    my $data    = {};
424
425  # If we have a url then we're specifying a specific theme (css) or repo (html)
426    # Pick up the file (html with <link>s or a css file with metadata)
427    my $user_agent = $app->new_ua;
428    my $request    = HTTP::Request->new( GET => $url );
429    my $response   = $user_agent->request($request);
430
431    # Make a repo if you've got a ton of links or an automagic entry if
432    # you're a css file
433    my $type = $response->headers->{'content-type'};
434    $type = shift @$type if ref $type eq 'ARRAY';
435    if ( $type =~ m!^text/css! ) {
436        $data->{auto}{url} = $url;
437        my $theme = fetch_theme( $url, ['collection:auto'] );
438        $data->{themes} = [$theme];
439    }
440    elsif ( $type =~ m!^text/html! ) {
441        my @repo_themes;
442        for my $link (
443            ref( $response->headers->{'link'} ) eq 'ARRAY'
444            ? @{ $response->headers->{'link'} }
445            : $response->headers->{'link'}
446          )
447        {
448            my ( $css, @parsed_link ) = split( /;/, $link );
449            $css =~ s/[<>]//g;
450            my %attr;
451            foreach (@parsed_link) {
452                my ( $name, $val ) = split /=/, $_, 2;
453                $name =~ s/^ //;
454                $val  =~ s/^['"]|['"]$//g;
455                next if $name eq '/';
456                $attr{ lc($name) } = $val;
457            }
458            next unless lc $attr{rel}  eq 'theme';
459            next unless lc $attr{type} eq 'text/x-theme';
460
461            # Fix for relative theme locations
462            if ($css !~ m!^https?://!) {
463                my $new_css = $url;
464                $new_css =~ s!/[a-z0-9_-]+\.[a-z]+?$|/$!/!;
465                $new_css .= $css;
466                $css = $new_css;
467            }
468            push @repo_themes, $css;
469        }
470
471        my $themes = [];
472        for my $repo_theme (@repo_themes) {
473            my $theme = fetch_theme( $repo_theme, [] );
474            push @$themes, $theme if $theme;
475        }
476        $data->{themes} = $themes;
477        if ( $data->{repo}{display_name} = $response->headers->{'title'} ) {
478            $data->{repo}{name} =
479              MT::Util::dirify( $data->{repo}{display_name} );
480        }
481        else {
482            $data->{repo}{display_name} = $url;
483            $data->{repo}{name}         = MT::Util::dirify($url);
484        }
485        $data->{repo}{url} = $url;
486    }
487    else {
488        return $app->error( $app->translate('Invalid URL: [_1]', $url) );
489    }
490
491    $data;
492}
493
494# sets up the object structure we return through json to populate
495# the mixer.
496sub make_themes {
497    my $app = MT->instance;
498
499    # categories
500    #   current    (for active theme)
501    #   repo       (for themes found at repo link)
502    #   my-designs (for themes that are stored locally)
503    #   mt-designs (for themes that are local and installed by default)
504    #   auto       (for link to a single css file)
505
506    # structure of "data"
507    #   categories => [ one, two, three ]  ie: 'current', 'repo'
508    #   themes => [
509    #       { theme }
510    #   ]
511    #   repo => {
512    #       display_name => 'display name',
513    #       name => 'repo name',
514    #       url => 'url of repo',
515    #   }
516
517# structure of "theme"
518#   theme => {
519#       name => 'theme_dir',
520#       imageSmall => 'link_to/thumbnail.gif',
521#       imageBig => 'link_to/thumbnail-large.gif',
522#       title => 'Theme Title',
523#       description => 'Theme description.',
524#       url_css => 'link_to/theme.css',
525#       url_zip => 'link_to/theme.zip',
526#       author => 'Author Name',
527#       author_url => 'http://author.com/'
528#       author_affiliation => 'Author Co.',
529#       layouts => "comma,delimited,layout,list"
530#       sort => 'theme_sortable_name',
531#       tags => ['association:tag']  ie, 'color:blue', 'designer:author', 'collection:repo'
532#   }
533
534    my ( $categories, $themes );
535    my $sys_root = File::Spec->catdir( $app->static_file_path, 'themes' );
536
537    # Generate our list of themes within the themeroot directory
538    my @sys_list = glob( File::Spec->catfile( $sys_root, "*" ) );
539    $categories->{'mt-designs'} = 1 if @sys_list;
540    for my $theme (@sys_list) {
541        my $theme_dir = $theme;
542        my $theme_url = $app->static_path . 'themes';
543        next unless -d $theme;
544        $theme =~ s/.*[\\\/]//;
545        $themes->{$theme} =
546          fetch_theme( $theme_dir, ['collection:mt-designs'], $theme_url,
547            $theme_dir );
548        $themes->{$theme}{name} = $themes->{$theme}{name};
549        $themes->{$theme}{prefix} = 'default';
550    }
551
552    my $themeroot =
553      File::Spec->catdir( $app->static_file_path, 'support', 'themes' );
554
555    # Generate our list of themes within the themeroot directory
556    my @themeroot_list = glob( File::Spec->catfile( $themeroot, "*" ) );
557    $categories->{'my-designs'} = 1 if @themeroot_list;
558    for my $theme (@themeroot_list) {
559        my $theme_dir = $theme;
560        next unless -d $theme;
561        $theme =~ s/.*[\\\/]//;
562        $themes->{$theme} =
563          fetch_theme( $theme_dir, ['collection:my-designs'] );
564        $themes->{$theme}{prefix} = 'local';
565    }
566
567    my $data = {
568        categories => [ keys %$categories ],
569        themes     => [ values %$themes ]
570    };
571
572    $data;
573}
574
575sub fetch_theme {
576    my $app = MT->app;
577    my ( $url, $tags, $baseurl, $basepath ) = @_;
578
579    my $theme;
580    my $stylesheet;
581    my $new_url;
582    my $themeroot;
583    if ( $url =~ m/^https?:/i ) {
584
585        # Pick up the css file
586        my $user_agent  = $app->new_ua;
587        my $css_request = HTTP::Request->new( GET => $url );
588        my $response    = $user_agent->request($css_request);
589        $stylesheet = $response->content if ($response->code >= 200) && ($response->code < 400);
590        return unless $stylesheet;
591
592# Break up the css url in to a couple useful pieces (generalize and break me out)
593        $theme = $url;
594        # discard any generic 'screen.css' filename
595        $theme =~ s!/screen.css$!!;
596        $theme =~ s/.*[\\\/]//;
597        my @url = split( /\//, $url );
598        for ( 0 .. ( scalar(@url) - 2 ) ) {
599            $new_url .= $url[$_] . '/';
600        }
601    }
602    else {
603        $themeroot = $basepath
604          || File::Spec->catdir( $app->static_file_path, 'support', 'themes' );
605        my $webthemeroot = $baseurl || $app->static_path . 'support/themes';
606
607        $theme = $url;
608        $theme =~ s/.*[\\\/]//;
609        my $file = File::Spec->catfile( $url, "$theme.css" );
610        $new_url = "$webthemeroot/$theme/";
611        if ( -e $file ) {
612            $stylesheet = file_mgr()->get_data($file);
613            $url        = $new_url . "$theme.css";
614        }
615        else {
616            $file = File::Spec->catfile( $url, "screen.css" );
617            if ( -e $file ) {
618                $stylesheet = file_mgr()->get_data($file);
619                $url        = $new_url . "screen.css";
620            }
621        }
622    }
623
624    # Pick up the metadata from the css
625    my @css_lines = split( /\r?\n/, $stylesheet || '' );
626    my $commented = 0;
627    my @comments;
628    for my $line (@css_lines) {
629        my $pos;
630        $pos = index( $line, "/*" );
631        unless ( $pos == -1 ) {
632            $line = substr( $line, $pos + 2 );
633            $commented = 1;
634        }
635        if ($commented) {
636            $pos = index( $line, "*/" );
637            unless ( $pos == -1 ) {
638                $line = substr( $line, 0, $pos );
639                $commented = 0;
640            }
641            push @comments, $line;
642        }
643    }
644
645    my $comment;
646    my %metadata;
647
648    # Trim me white space, yarr
649    for (@comments) {
650
651        # TBD: strip any "risky" content; we don't want any
652        # XSS in this content.
653        # Strip any null bytes
654        tr/\x00//d;
655        s/^\s+|\s+$//g;
656        my ( $key, $value ) = split( /:/, $_, 2 ) or next;
657        next unless defined $value;
658        $value =~ s/^\s+//;
659        $metadata{ lc $key } = $value;
660    }
661
662    my $thumbnail_link;
663    $thumbnail_link = $new_url . 'thumbnail.gif';
664    my $thumbnail_large_link;
665    $thumbnail_large_link = $new_url . 'thumbnail-large.gif';
666
667    require MT::Util;
668    my $data = {
669        name        => $theme,
670        description => $metadata{description} || '',
671        title       => $metadata{name} || '(Untitled)',
672        url         => $url,
673        imageSmall  => $thumbnail_link,
674        imageBig    => $thumbnail_large_link,
675        author      => $metadata{designer} || $metadata{author} || '',
676        author_url  => $metadata{designer_url} || $metadata{author_url} || '',
677        author_affiliation => $metadata{author_affiliation} || '',
678        layouts            => $metadata{layouts} || '',
679        'sort'             => $metadata{name} || '',
680        tags               => $tags,
681        blogs              => [],
682    };
683    $data;
684}
685
686sub plugin {
687    return MT->component('StyleCatcher');
688}
689
6901;
Note: See TracBrowser for help on using the browser.