root/branches/release-41/lib/MT/Component.pm @ 2667

Revision 2667, 17.9 kB (checked in by bchoate, 17 months ago)

Added support for a plugin/component to define an init handler.

  • Property svn:keywords set to Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::Component;
8
9use strict;
10use base qw( Class::Accessor::Fast MT::ErrorHandler );
11use MT::Util qw( encode_js weaken );
12
13__PACKAGE__->mk_accessors(qw( id path envelope version schema_version ));
14
15#BEGIN {
16#
17#    # my @registry_methods = qw( callbacks upgrade_functions object_types
18#    #     junk_filters text_filters permissions importers rebuild_options
19#    #     tasks );
20#    my @registry_methods = qw( callbacks );
21#    foreach my $meth (@registry_methods) {
22#        my $sub = sub { &_getset( shift, $meth, @_ ) };
23#        no strict 'refs';
24#        *$meth = $sub;
25#    }
26#}
27
28# static method
29sub select {
30    my ( $pkg, $class ) = @_;
31    if ( $class && $class !~ m/::/ ) {
32        $class = $pkg . '::' . $class;
33    }
34    elsif ( !$class ) {
35        $class = $pkg;
36    }
37    return @MT::Components if $class eq 'MT::Component';
38    return @MT::Plugins if $class eq 'MT::Plugin';
39    return grep { UNIVERSAL::isa( $_, $class ) } @MT::Components;
40}
41
42sub new {
43    my $class = shift;
44    my ($self) = ref $_[0] ? @_ : {@_};
45    bless $self, $class;
46    $self->init();
47    $self;
48}
49
50sub init {
51    my $c = shift;
52    $c->init_registry() or return;
53
54    # plugin callbacks are initialized after they finish loading.
55    $c->init_callbacks() unless $c->isa('MT::Plugin');
56    $c;
57}
58
59sub init_callbacks {
60    my $c = shift;
61
62    # Tricky; we only want to add this callback IF the plugin
63    # has an init_app callback of it's own; at the same time
64    # we have to declare a superclass init_app method so if
65    # the plugin uses $plugin->SUPER::init_app(), it works.
66    # So we test here to see if the signature of the init_app
67    # method is something other than our stub init_app
68    # method in MT::Component (same goes for init_request below)
69    if ( $c->can('init_app') != \&MT::Component::init_app ) {
70        MT->add_callback(
71            'init_app',
72            5, $c,
73            sub {
74                my $cb = shift;
75                local $MT::plugin_registry = $c->{registry};
76                $c->init_app(@_);
77            }
78        );
79    }
80    elsif (_getset( $c, 'init_app' )
81        || _getset( $c, 'applications' ) )
82    {
83        MT->add_callback( 'init_app', 5, $c, \&on_init_app );
84    }
85
86    if ( $c->can('init_request') != \&MT::Component::init_request ) {
87        MT->add_callback(
88            'init_request',
89            5, $c,
90            sub {
91                my $cb = shift;
92                local $MT::plugin_registry = $c->{registry};
93                $c->init_request(@_);
94            }
95        );
96    }
97    elsif ( _getset( $c, 'init_request' ) || _getset( $c, 'applications' ) ) {
98        MT->add_callback( 'init_request', 5, $c, \&on_init_request );
99    }
100
101    if ( $c->{init_tasks} ) {
102        MT->add_callback( 'tasks', 5, $c, $c->{init_tasks} );
103    }
104    elsif ( $c->can('init_tasks') ) {
105        MT->add_callback( 'tasks', 5, $c,
106            sub { my $cb = shift; $c->init_tasks(@_) } );
107    }
108
109    if ( my $callbacks = $c->callbacks ) {
110        if ( ref $callbacks eq 'ARRAY' ) {
111            foreach my $cb (@$callbacks) {
112                MT->add_callback( $_->{name}, $cb->{priority} || 5,
113                    $c, $cb->{handler} || $cb->{code} );
114            }
115        }
116        elsif ( ref $callbacks eq 'HASH' ) {
117            foreach my $cbname ( keys %$callbacks ) {
118                if ( ref $callbacks->{$cbname} eq 'CODE' ||  (ref $callbacks->{$cbname} eq '' && $callbacks->{$cbname})) {
119                    MT->add_callback( $cbname, 5, $c, $callbacks->{$cbname} );
120                }
121                elsif ( ref $callbacks->{$cbname} eq 'HASH' ) {
122                    MT->add_callback(
123                        $callbacks->{$cbname}{callback} || $cbname,
124                        $callbacks->{$cbname}{priority} || 5,
125                        $c,
126                        $callbacks->{$cbname}{handler}
127                          || $callbacks->{$cbname}{code}
128                    );
129                }
130                elsif ( ref $callbacks->{$cbname} eq 'ARRAY' ) {
131                    my $list = $callbacks->{$cbname};
132                    MT->add_callback( $cbname, $_->{priority} || 5,
133                        $c, $_->{handler} || $_->{code} )
134                      foreach @$list;
135                }
136            }
137        }
138    }
139    if (my $init = _getset( $c, 'init' )) {
140        if ( $init && !ref($init) ) {
141            $init = MT->handler_to_coderef($init);
142        }
143        return $init->($c);
144    }
145    return 1;
146}
147
148sub load_registry {
149    my $c      = shift;
150    my ($file) = @_;
151    my $path   = $c->path or return;
152    $path = File::Spec->catfile( $c->path, $file );
153    return unless -f $path;
154    require YAML::Tiny;
155    my $y = eval { YAML::Tiny->read($path) }
156        or die "Error reading $path: " . $YAML::Tiny::errstr;
157    if ( ref($y) ) {
158
159        # skip over non-hash elements
160        shift @$y while @$y && ( ref( $y->[0] ) ne 'HASH' );
161        return $y->[0] if @$y;
162    }
163    return {};
164}
165
166sub init_registry {
167    my $c = shift;
168    my $r = $c->load_registry("config.yaml");
169    if ( !$r ) {
170        return 1;
171    }
172
173   # TBD: 'extends' support...
174   # if (my $ext = $r->{extends}) {
175   #     # require any other components declared here
176   #     $ext = [ $ext ] unless ref($ext) eq 'ARRAY';
177   #     foreach my $comp (@$ext) {
178   #         MT->require_component($comp)
179   #             or return $c->error("Error loading required component: $comp");
180   #     }
181   # }
182    $c->registry($r);
183
184    # map key registry elements into metadata
185    foreach my $prop (qw(version schema_version)) {
186        $c->$prop( $r->{$prop} ) if exists $r->{$prop};
187    }
188    $c->name( $r->{label} ) if exists $r->{label};
189    return 1;
190}
191
192sub callbacks {
193    my $c = shift;
194    my $root_cb = _getset($c, 'callbacks') || {};
195    my $apps = _getset($c, 'applications');
196    for my $app (keys %$apps) {
197        my @path = qw( applications );
198        push @path, $app;
199        my $r = $c->registry( @path );
200        if ($r) {
201            my $cb = _getset($r, 'callbacks') || {};
202            MT::__merge_hash($root_cb, $cb);
203        }
204    }
205    return $root_cb;
206}
207
208# STUB
209sub init_app { }
210sub init_request { }
211
212sub on_init_app {
213    my $cb      = shift;
214    my $c       = $cb->plugin;
215    my ($app)   = @_;
216    my $app_pkg = ref $app;
217    my $init;
218    if ( $init = _getset( $c, 'init_app' ) ) {
219        if ( ref $init eq 'HASH' ) {
220            $init = $init->{$app_pkg};
221        }
222    }
223    else {
224        $init = $c->registry( "applications", $app->id, "init" );
225    }
226    if ( $init && !ref($init) ) {
227        $init = MT->handler_to_coderef($init);
228    }
229    if ( $init && ( ref $init eq 'CODE' ) ) {
230        local $MT::plugin_registry = $c->{registry};
231        $init->( $c, @_ );
232    }
233    elsif ( $c->can('init_app') ) {
234        local $MT::plugin_registry = $c->{registry};
235        $c->init_app(@_);
236    }
237}
238
239sub on_init_request {
240    my $cb      = shift;
241    my $c       = $cb->plugin;
242    my ($app)   = @_;
243    my $app_pkg = ref $app;
244    my $init;
245    if ( $init = _getset( $c, 'init_request' ) ) {
246        if ( ref $init eq 'HASH' ) {
247            $init = $init->{$app_pkg} if exists $init->{$app_pkg};
248        }
249    }
250    else {
251        $init = $c->registry( "applications", $app->id, "init_request" );
252    }
253    if ( $init && !ref($init) ) {
254        $init = MT->handler_to_coderef($init);
255    }
256    if ( ref $init eq 'CODE' ) {
257        local $MT::plugin_registry = $c->{registry};
258        $init->( $app, @_ );
259    }
260}
261
262sub _getset {
263    my $c = shift;
264    my ($prop) = @_;
265    if ( exists $c->{registry}{$prop} ) {
266        if ( @_ > 1 ) {
267            return $c->{registry}{$prop} = $_[1];
268        }
269        else {
270            my $out = $c->{registry}{$prop};
271
272            # Handle reference to another YAML file
273            # (ie, app-cms.yaml/tags.yaml/etc.)
274            if ( defined($out) && !ref($out) && ( $out =~ m/^[-\w]+\.yaml$/ ) )
275            {
276                my $r = $c->load_registry($out);
277                if ($r) {
278                    return $c->{registry}{$prop} = $r;
279                }
280                return undef;
281            }
282            return $out;
283        }
284    }
285    return @_ > 1 ? $c->{$prop} = $_[1] : $c->{$prop};
286}
287
288sub _getset_translate {
289    my $c = shift;
290    my ($p) = @_;
291    if ( !$p ) {
292        $p = ( caller(1) )[3];
293        $p =~ s/.*:://;
294    }
295    my $return;
296    if ( exists $c->{registry}{$p} ) {
297        $return = @_ > 1 ? $c->{registry}{$p} = $_[1] : $c->{registry}{$p};
298    }
299    else {
300        $return = @_ > 1 ? $c->{$p} = $_[1] : $c->{$p};
301    }
302    return $c->l10n_filter( defined $return ? $return : '' );
303}
304
305sub name { &_getset_translate }
306
307sub label {
308    my $c = shift;
309    return $c->_getset('label') || $c->name();
310}
311
312sub description { &_getset_translate }
313
314sub needs_upgrade {
315    my $c  = shift;
316    my $sv = $c->schema_version;
317    return 0 unless defined $sv;
318    my $key     = 'PluginSchemaVersion';
319    my $id      = $c->id;
320    my $ver     = MT->config($key);
321    my $cfg_ver = $ver->{$id} if $ver;
322    if ( ( !defined $cfg_ver ) || ( $cfg_ver < $sv ) ) {
323        return 1;
324    }
325    0;
326}
327
328sub template_paths {
329    my $c = shift;
330
331    my $mt   = MT->instance;
332    my $path = $mt->config('TemplatePath');
333
334    my @paths;
335    my $dir = File::Spec->catdir( $c->path, 'tmpl' );
336    push @paths, $dir if -d $dir;
337    $dir = $c->path;
338    push @paths, $dir if -d $dir;
339    if ( my $alt_path = $mt->config('AltTemplatePath') ) {
340        if ( -d $alt_path ) {    # AltTemplatePath is absolute
341            push @paths, File::Spec->catdir( $alt_path, $mt->{template_dir} )
342              if $mt->{template_dir};
343            push @paths, $alt_path;
344        }
345    }
346    push @paths, File::Spec->catdir( $path, $mt->{template_dir} )
347      if $mt->{template_dir};
348    push @paths, $path;
349    return @paths;
350}
351
352sub load_tmpl {
353    my $c = shift;
354    my ($file, $param) = @_;
355
356    my $mt = MT->instance;
357    my $type = { 'SCALAR' => 'scalarref', 'ARRAY' => 'arrayref' }->{ ref $file }
358      || 'filename';
359
360    require MT::Template;
361    my $tmpl = MT::Template->new(
362        type   => $type,
363        source => $file,
364        path   => [ $c->template_paths ],
365        ($mt->isa('MT::App') ? ( filter => sub {
366            my ($str, $fname) = @_;
367            if ($fname) {
368                $fname = File::Basename::basename($fname);
369                $fname =~ s/\.tmpl$//;
370                $mt->run_callbacks("template_source.$fname", $mt, @_);
371            } else {
372                $mt->run_callbacks("template_source", $mt, @_);
373            }
374            return $str;
375        }) : ()),
376    );
377    return $c->error(
378        $mt->translate( "Loading template '[_1]' failed: [_2]", $file, MT::Template->errstr ) )
379      unless defined $tmpl;
380    my $text = $tmpl->text;
381    if (($text =~ m/<(mt|_)_trans/i) && ($c->id)) {
382        $tmpl->text( '<__trans_section component="' . $c->id . '">' . $text . '</__trans_section>');
383    }
384    $tmpl->{__file} = $file if $type eq 'filename';
385
386    ## We do this in load_tmpl because show_error and login don't call
387    ## build_page; so we need to set these variables here.
388    $mt->set_default_tmpl_params($tmpl);
389    $tmpl->param($param) if $param;
390
391    return $tmpl;
392}
393
394sub l10n_class { _getset( shift, 'l10n_class', @_ ) || 'MT::L10N' }
395
396sub translate {
397    my $c       = shift;
398    my $handles = MT->request('l10n_handle') || {};
399    my $h       = $handles->{ $c->id };
400    unless ($h) {
401        my $lang = MT->current_language || MT->config->DefaultLanguage;
402        eval "require " . $c->l10n_class . ";";
403        if ($@) {
404            $h = MT->language_handle;
405        }
406        else {
407            $h = $c->l10n_class->get_handle($lang);
408        }
409        $handles->{ $c->id } = $h;
410        MT->request( 'l10n_handle', $handles );
411    }
412    my ( $format, @args ) = @_;
413    foreach (@args) {
414        $_ = $_->() if ref($_) eq 'CODE';
415    }
416    my $enc = MT->instance->config('PublishCharset');
417    my $str;
418    if ($h) {
419        if ( $enc =~ m/utf-?8/i ) {
420            $str = $h->maketext( $format, @args );
421        }
422        else {
423            $str = MT::I18N::encode_text(
424                $h->maketext(
425                    $format,
426                    map { MT::I18N::encode_text( $_, $enc, 'utf-8' ) } @args
427                ),
428                'utf-8', $enc
429            );
430        }
431    }
432    if ( !defined $str ) {
433        $str = MT->translate(@_);
434    }
435    $str;
436}
437
438sub translate_templatized {
439    my $c = shift;
440    my ($text) = @_;
441    my @cstack;
442    while (1) {
443        $text =~ s!(<(/)?(?:_|MT)_TRANS(_SECTION)?(?:(?:\s+((?:\w+)\s*=\s*(["'])(?:(<(?:[^"'>]|"[^"]*"|'[^']*')+)?>|[^\5]+?)*?\5))+?\s*/?)?>)!
444        my($msg, $close, $section, %args) = ($1, $2, $3);
445        while ($msg =~ /\b(\w+)\s*=\s*(["'])((?:<(?:[^"'>]|"[^"]*"|'[^']*')+?>|[^\2])*?)?\2/g) {  #"
446            $args{$1} = $3;
447        }
448        if ($section) {
449            if ($close) {
450                $c = pop @cstack;
451            } else {
452                if ($args{component}) {
453                    push @cstack, $c;
454                    $c = MT->component($args{component});
455                }
456                else {
457                    die "__trans_section without a component argument";
458                }
459            }
460            '';
461        } else {
462            $args{params} = '' unless defined $args{params};
463            my @p = map MT::Util::decode_html($_),
464                    split /\s*%%\s*/, $args{params}, -1;
465            @p = ('') unless @p;
466            my $translation = $c->translate($args{phrase}, @p);
467            if (exists $args{escape}) {
468                if (lc($args{escape}) eq 'html') {
469                    $translation = MT::Util::encode_html($translation);
470                } elsif (lc($args{escape}) eq 'url') {
471                    $translation = MT::Util::encode_url($translation);
472                } else {
473                    # fallback for js/javascript/singlequotes
474                    $translation = encode_js($translation);
475                }
476            }
477            $translation;
478        }
479        !igem or last;
480    }
481    return $text;
482}
483
484sub l10n_filter { $_[0]->translate_templatized( $_[1] ) }
485
486# can be invoked statically or with an instance.
487# if invoked statically, it queries all available plugins
488#   MT::Plugin->registry("applications")
489# if invoked with an instance, it only selects for that plugin's registry
490#   $foo_plugin->registry("applications")
491
492sub registry {
493    my $c = shift;
494    if ( ref $c ) {
495        if ( !@_ ) { return $c->{registry} ||= {} }
496        if ( ref $_[0] ) {
497            return $c->{registry} = shift;
498        }
499        my @path = @_;
500        my $r    = $c->{registry};
501        return undef unless $r;
502        my ( $last_r, $last_p );
503        foreach my $p (@path) {
504            if ( ref $p ) {
505
506                # Handle the case where an assignment
507                # is being made to a registry item. Ie
508                # $comp->registry("foo","bar","baz", { stuff => ... })
509                $last_r->{$last_p} = $p;
510                $r = $last_r;
511                last;
512            }
513            if ( exists $r->{$p} ) {
514                my $v = $r->{$p};
515
516                # check for a yaml file reference...
517                if ( !ref($v) ) {
518                    if ( $v =~ m/^[-\w]+\.yaml$/ ) {
519                        my $f = File::Spec->catfile( $c->path, $v );
520                        if ( -f $f ) {
521                            require YAML::Tiny;
522                            my $y = eval { YAML::Tiny->read($f) }
523                                or die "Error reading $f: "
524                                    . $YAML::Tiny::errstr;
525                            # skip over non-hash elements
526                            shift @$y
527                                while @$y && ( ref( $y->[0] ) ne 'HASH' );
528                            $r->{$p} = $y->[0] if @$y;
529                        }
530                    } elsif ($v =~ m/^\$\w+::/) {
531                        my $code = MT->handler_to_coderef($v);
532                        if (ref $code eq 'CODE') {
533                            $r->{$p} = $code->($c);
534                        }
535                    }
536                }
537                elsif ( ref($v) eq 'CODE' ) {
538                    $r->{$p} = $v->($c);
539                }
540                $last_r = $r;
541                $last_p = $p;
542                $r      = $r->{$p};
543            }
544            else {
545                return undef;
546            }
547        }
548
549        # deepscan for any label elements since they will need translation
550        if ( ref $r eq 'HASH' ) {
551            __deep_localize_labels( $c, $r );
552            weaken($_->{plugin} = $c)
553                for grep { ref $_ eq 'HASH' } values %$r;
554        }
555
556        # $r should now be the element of the path requested
557        return $r;
558    }
559    else {
560        my @objs = $c->select();
561        my @list;
562        foreach my $o (@objs) {
563            my $r = $o->registry(@_);
564            push @list, $r if defined $r;
565        }
566        return @list ? \@list : undef;
567    }
568}
569
570sub __deep_localize_labels {
571    my ( $c, $hash ) = @_;
572    foreach my $k ( keys %$hash ) {
573        if ( ref( $hash->{$k} ) eq 'HASH' ) {
574            __deep_localize_labels( $c, $hash->{$k} );
575        }
576        else {
577            next unless $k =~ m/(?:\b|_)label\b/;
578            if ( !ref( my $label = $hash->{$k} ) ) {
579                $hash->{$k} = sub { $c->translate($label) };
580            }
581        }
582    }
583}
584
5851;
586__END__
587
588=head1 NAME
589
590MT::Component - Movable Type class that describes a component.
591
592=head1 SYNOPSIS
593
594=head1 DESCRIPTION
595
596=head1 ARGUMENTS
597
598=over 4
599
600=item * id (recommended)
601
602=item * label
603
604=item * version
605
606The version number for the release of the plugin. Will be displayed
607next to the plugin's name wherever listed. This information is not
608required, but recommended.
609
610=item * schema_version
611
612If your plugin declares a list of object classes, the schema_version
613is used to determine whether your classes require installation or
614upgrade. MT will store your plugin's schema_version in the C<MT::Config>
615table for future reference.
616
617=back
618
619=head1 METHODS
620
621=head1 LOCALIZATION
622
623=head1 AUTHOR & COPYRIGHT
624
625Please see L<MT/AUTHOR & COPYRIGHT>.
626
627=cut
Note: See TracBrowser for help on using the browser.