root/branches/release-35/lib/MT/Component.pm @ 1927

Revision 1927, 17.7 kB (checked in by mpaschal, 20 months ago)

Land the new implementation of metadata based on narrow tables
BugzID: 68749

  • 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}
140
141sub load_registry {
142    my $c      = shift;
143    my ($file) = @_;
144    my $path   = $c->path or return;
145    $path = File::Spec->catfile( $c->path, $file );
146    return unless -f $path;
147    require YAML::Tiny;
148    my $y = eval { YAML::Tiny->read($path) }
149        or die "Error reading $path: " . $YAML::Tiny::errstr;
150    if ( ref($y) ) {
151
152        # skip over non-hash elements
153        shift @$y while @$y && ( ref( $y->[0] ) ne 'HASH' );
154        return $y->[0] if @$y;
155    }
156    return {};
157}
158
159sub init_registry {
160    my $c = shift;
161    my $r = $c->load_registry("config.yaml");
162    if ( !$r ) {
163        return 1;
164    }
165
166   # TBD: 'extends' support...
167   # if (my $ext = $r->{extends}) {
168   #     # require any other components declared here
169   #     $ext = [ $ext ] unless ref($ext) eq 'ARRAY';
170   #     foreach my $comp (@$ext) {
171   #         MT->require_component($comp)
172   #             or return $c->error("Error loading required component: $comp");
173   #     }
174   # }
175    $c->registry($r);
176
177    # map key registry elements into metadata
178    foreach my $prop (qw(version schema_version)) {
179        $c->$prop( $r->{$prop} ) if exists $r->{$prop};
180    }
181    $c->name( $r->{label} ) if exists $r->{label};
182    return 1;
183}
184
185sub callbacks {
186    my $c = shift;
187    my $root_cb = _getset($c, 'callbacks') || {};
188    my $apps = _getset($c, 'applications');
189    for my $app (keys %$apps) {
190        my @path = qw( applications );
191        push @path, $app;
192        my $r = $c->registry( @path );
193        if ($r) {
194            my $cb = _getset($r, 'callbacks') || {};
195            MT::__merge_hash($root_cb, $cb);
196        }
197    }
198    return $root_cb;
199}
200
201# STUB
202sub init_app { }
203sub init_request { }
204
205sub on_init_app {
206    my $cb      = shift;
207    my $c       = $cb->plugin;
208    my ($app)   = @_;
209    my $app_pkg = ref $app;
210    my $init;
211    if ( $init = _getset( $c, 'init_app' ) ) {
212        if ( ref $init eq 'HASH' ) {
213            $init = $init->{$app_pkg};
214        }
215    }
216    else {
217        $init = $c->registry( "applications", $app->id, "init" );
218    }
219    if ( $init && !ref($init) ) {
220        $init = MT->handler_to_coderef($init);
221    }
222    if ( $init && ( ref $init eq 'CODE' ) ) {
223        local $MT::plugin_registry = $c->{registry};
224        $init->( $c, @_ );
225    }
226    elsif ( $c->can('init_app') ) {
227        local $MT::plugin_registry = $c->{registry};
228        $c->init_app(@_);
229    }
230}
231
232sub on_init_request {
233    my $cb      = shift;
234    my $c       = $cb->plugin;
235    my ($app)   = @_;
236    my $app_pkg = ref $app;
237    my $init;
238    if ( $init = _getset( $c, 'init_request' ) ) {
239        if ( ref $init eq 'HASH' ) {
240            $init = $init->{$app_pkg} if exists $init->{$app_pkg};
241        }
242    }
243    else {
244        $init = $c->registry( "applications", $app->id, "init_request" );
245    }
246    if ( $init && !ref($init) ) {
247        $init = MT->handler_to_coderef($init);
248    }
249    if ( ref $init eq 'CODE' ) {
250        local $MT::plugin_registry = $c->{registry};
251        $init->( $app, @_ );
252    }
253}
254
255sub _getset {
256    my $c = shift;
257    my ($prop) = @_;
258    if ( exists $c->{registry}{$prop} ) {
259        if ( @_ > 1 ) {
260            return $c->{registry}{$prop} = $_[1];
261        }
262        else {
263            my $out = $c->{registry}{$prop};
264
265            # Handle reference to another YAML file
266            # (ie, app-cms.yaml/tags.yaml/etc.)
267            if ( defined($out) && !ref($out) && ( $out =~ m/^[-\w]+\.yaml$/ ) )
268            {
269                my $r = $c->load_registry($out);
270                if ($r) {
271                    return $c->{registry}{$prop} = $r;
272                }
273                return undef;
274            }
275            return $out;
276        }
277    }
278    return @_ > 1 ? $c->{$prop} = $_[1] : $c->{$prop};
279}
280
281sub _getset_translate {
282    my $c = shift;
283    my ($p) = @_;
284    if ( !$p ) {
285        $p = ( caller(1) )[3];
286        $p =~ s/.*:://;
287    }
288    my $return;
289    if ( exists $c->{registry}{$p} ) {
290        $return = @_ > 1 ? $c->{registry}{$p} = $_[1] : $c->{registry}{$p};
291    }
292    else {
293        $return = @_ > 1 ? $c->{$p} = $_[1] : $c->{$p};
294    }
295    return $c->l10n_filter( defined $return ? $return : '' );
296}
297
298sub name { &_getset_translate }
299
300sub label {
301    my $c = shift;
302    return $c->_getset('label') || $c->name();
303}
304
305sub description { &_getset_translate }
306
307sub needs_upgrade {
308    my $c  = shift;
309    my $sv = $c->schema_version;
310    return 0 unless defined $sv;
311    my $key     = 'PluginSchemaVersion';
312    my $id      = $c->id;
313    my $ver     = MT->config($key);
314    my $cfg_ver = $ver->{$id} if $ver;
315    if ( ( !defined $cfg_ver ) || ( $cfg_ver < $sv ) ) {
316        return 1;
317    }
318    0;
319}
320
321sub template_paths {
322    my $c = shift;
323
324    my $mt   = MT->instance;
325    my $path = $mt->config('TemplatePath');
326
327    my @paths;
328    my $dir = File::Spec->catdir( $c->path, 'tmpl' );
329    push @paths, $dir if -d $dir;
330    $dir = $c->path;
331    push @paths, $dir if -d $dir;
332    if ( my $alt_path = $mt->config('AltTemplatePath') ) {
333        if ( -d $alt_path ) {    # AltTemplatePath is absolute
334            push @paths, File::Spec->catdir( $alt_path, $mt->{template_dir} )
335              if $mt->{template_dir};
336            push @paths, $alt_path;
337        }
338    }
339    push @paths, File::Spec->catdir( $path, $mt->{template_dir} )
340      if $mt->{template_dir};
341    push @paths, $path;
342    return @paths;
343}
344
345sub load_tmpl {
346    my $c = shift;
347    my ($file, $param) = @_;
348
349    my $mt = MT->instance;
350    my $type = { 'SCALAR' => 'scalarref', 'ARRAY' => 'arrayref' }->{ ref $file }
351      || 'filename';
352
353    require MT::Template;
354    my $tmpl = MT::Template->new(
355        type   => $type,
356        source => $file,
357        path   => [ $c->template_paths ],
358        ($mt->isa('MT::App') ? ( filter => sub {
359            my ($str, $fname) = @_;
360            if ($fname) {
361                $fname = File::Basename::basename($fname);
362                $fname =~ s/\.tmpl$//;
363                $mt->run_callbacks("template_source.$fname", $mt, @_);
364            } else {
365                $mt->run_callbacks("template_source", $mt, @_);
366            }
367            return $str;
368        }) : ()),
369    );
370    return $c->error(
371        $mt->translate( "Loading template '[_1]' failed: [_2]", $file, MT::Template->errstr ) )
372      unless defined $tmpl;
373    my $text = $tmpl->text;
374    if (($text =~ m/<(mt|_)_trans/i) && ($c->id)) {
375        $tmpl->text( '<__trans_section component="' . $c->id . '">' . $text . '</__trans_section>');
376    }
377    $tmpl->{__file} = $file if $type eq 'filename';
378
379    ## We do this in load_tmpl because show_error and login don't call
380    ## build_page; so we need to set these variables here.
381    $mt->set_default_tmpl_params($tmpl);
382    $tmpl->param($param) if $param;
383
384    return $tmpl;
385}
386
387sub l10n_class { _getset( shift, 'l10n_class', @_ ) || 'MT::L10N' }
388
389sub translate {
390    my $c       = shift;
391    my $handles = MT->request('l10n_handle') || {};
392    my $h       = $handles->{ $c->id };
393    unless ($h) {
394        my $lang = MT->current_language || MT->config->DefaultLanguage;
395        eval "require " . $c->l10n_class . ";";
396        if ($@) {
397            $h = MT->language_handle;
398        }
399        else {
400            $h = $c->l10n_class->get_handle($lang);
401        }
402        $handles->{ $c->id } = $h;
403        MT->request( 'l10n_handle', $handles );
404    }
405    my ( $format, @args ) = @_;
406    foreach (@args) {
407        $_ = $_->() if ref($_) eq 'CODE';
408    }
409    my $enc = MT->instance->config('PublishCharset');
410    my $str;
411    if ($h) {
412        if ( $enc =~ m/utf-?8/i ) {
413            $str = $h->maketext( $format, @args );
414        }
415        else {
416            $str = MT::I18N::encode_text(
417                $h->maketext(
418                    $format,
419                    map { MT::I18N::encode_text( $_, $enc, 'utf-8' ) } @args
420                ),
421                'utf-8', $enc
422            );
423        }
424    }
425    if ( !defined $str ) {
426        $str = MT->translate(@_);
427    }
428    $str;
429}
430
431sub translate_templatized {
432    my $c = shift;
433    my ($text) = @_;
434    my @cstack;
435    while (1) {
436        $text =~ s!(<(/)?(?:_|MT)_TRANS(_SECTION)?(?:(?:\s+((?:\w+)\s*=\s*(["'])(?:(<(?:[^"'>]|"[^"]*"|'[^']*')+)?>|[^\5]+?)*?\5))+?\s*/?)?>)!
437        my($msg, $close, $section, %args) = ($1, $2, $3);
438        while ($msg =~ /\b(\w+)\s*=\s*(["'])((?:<(?:[^"'>]|"[^"]*"|'[^']*')+?>|[^\2])*?)?\2/g) {  #"
439            $args{$1} = $3;
440        }
441        if ($section) {
442            if ($close) {
443                $c = pop @cstack;
444            } else {
445                if ($args{component}) {
446                    push @cstack, $c;
447                    $c = MT->component($args{component});
448                }
449                else {
450                    die "__trans_section without a component argument";
451                }
452            }
453            '';
454        } else {
455            $args{params} = '' unless defined $args{params};
456            my @p = map MT::Util::decode_html($_),
457                    split /\s*%%\s*/, $args{params}, -1;
458            @p = ('') unless @p;
459            my $translation = $c->translate($args{phrase}, @p);
460            if (exists $args{escape}) {
461                if (lc($args{escape}) eq 'html') {
462                    $translation = MT::Util::encode_html($translation);
463                } elsif (lc($args{escape}) eq 'url') {
464                    $translation = MT::Util::encode_url($translation);
465                } else {
466                    # fallback for js/javascript/singlequotes
467                    $translation = encode_js($translation);
468                }
469            }
470            $translation;
471        }
472        !igem or last;
473    }
474    return $text;
475}
476
477sub l10n_filter { $_[0]->translate_templatized( $_[1] ) }
478
479# can be invoked statically or with an instance.
480# if invoked statically, it queries all available plugins
481#   MT::Plugin->registry("applications")
482# if invoked with an instance, it only selects for that plugin's registry
483#   $foo_plugin->registry("applications")
484
485sub registry {
486    my $c = shift;
487    if ( ref $c ) {
488        if ( !@_ ) { return $c->{registry} ||= {} }
489        if ( ref $_[0] ) {
490            return $c->{registry} = shift;
491        }
492        my @path = @_;
493        my $r    = $c->{registry};
494        return undef unless $r;
495        my ( $last_r, $last_p );
496        foreach my $p (@path) {
497            if ( ref $p ) {
498
499                # Handle the case where an assignment
500                # is being made to a registry item. Ie
501                # $comp->registry("foo","bar","baz", { stuff => ... })
502                $last_r->{$last_p} = $p;
503                $r = $last_r;
504                last;
505            }
506            if ( exists $r->{$p} ) {
507                my $v = $r->{$p};
508
509                # check for a yaml file reference...
510                if ( !ref($v) ) {
511                    if ( $v =~ m/^[-\w]+\.yaml$/ ) {
512                        my $f = File::Spec->catfile( $c->path, $v );
513                        if ( -f $f ) {
514                            require YAML::Tiny;
515                            my $y = eval { YAML::Tiny->read($f) }
516                                or die "Error reading $f: "
517                                    . $YAML::Tiny::errstr;
518                            # skip over non-hash elements
519                            shift @$y
520                                while @$y && ( ref( $y->[0] ) ne 'HASH' );
521                            $r->{$p} = $y->[0] if @$y;
522                        }
523                    } elsif ($v =~ m/^\$\w+::/) {
524                        my $code = MT->handler_to_coderef($v);
525                        if (ref $code eq 'CODE') {
526                            $r->{$p} = $code->($c);
527                        }
528                    }
529                }
530                elsif ( ref($v) eq 'CODE' ) {
531                    $r->{$p} = $v->($c);
532                }
533                $last_r = $r;
534                $last_p = $p;
535                $r      = $r->{$p};
536            }
537            else {
538                return undef;
539            }
540        }
541
542        # deepscan for any label elements since they will need translation
543        if ( ref $r eq 'HASH' ) {
544            __deep_localize_labels( $c, $r );
545            weaken($_->{plugin} = $c)
546                for grep { ref $_ eq 'HASH' } values %$r;
547        }
548
549        # $r should now be the element of the path requested
550        return $r;
551    }
552    else {
553        my @objs = $c->select();
554        my @list;
555        foreach my $o (@objs) {
556            my $r = $o->registry(@_);
557            push @list, $r if defined $r;
558        }
559        return @list ? \@list : undef;
560    }
561}
562
563sub __deep_localize_labels {
564    my ( $c, $hash ) = @_;
565    foreach my $k ( keys %$hash ) {
566        if ( ref( $hash->{$k} ) eq 'HASH' ) {
567            __deep_localize_labels( $c, $hash->{$k} );
568        }
569        else {
570            next unless $k =~ m/(?:\b|_)label\b/;
571            if ( !ref( my $label = $hash->{$k} ) ) {
572                $hash->{$k} = sub { $c->translate($label) };
573            }
574        }
575    }
576}
577
5781;
579__END__
580
581=head1 NAME
582
583MT::Component - Movable Type class that describes a component.
584
585=head1 SYNOPSIS
586
587=head1 DESCRIPTION
588
589=head1 ARGUMENTS
590
591=over 4
592
593=item * id (recommended)
594
595=item * label
596
597=item * version
598
599The version number for the release of the plugin. Will be displayed
600next to the plugin's name wherever listed. This information is not
601required, but recommended.
602
603=item * schema_version
604
605If your plugin declares a list of object classes, the schema_version
606is used to determine whether your classes require installation or
607upgrade. MT will store your plugin's schema_version in the C<MT::Config>
608table for future reference.
609
610=back
611
612=head1 METHODS
613
614=head1 LOCALIZATION
615
616=head1 AUTHOR & COPYRIGHT
617
618Please see L<MT/AUTHOR & COPYRIGHT>.
619
620=cut
Note: See TracBrowser for help on using the browser.