root/trunk/lib/MT/Upgrade.pm @ 3082

Revision 3082, 45.7 kB (checked in by bchoate, 14 months ago)

Merging fireball branch changes to-date to trunk: svn merge -r2974:3081 http://code.sixapart.com/svn/movabletype/branches/fireball .

  • 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::Upgrade;
8
9use strict;
10use base qw( MT::ErrorHandler );
11use File::Spec;
12
13# The upgrade process...
14#
15#    * Database check of all data types
16#      - assign default values for 'null' columns
17#    * Template check for all blogs
18
19use vars qw(%classes %functions %LegacyPerms $App $DryRun $Installing $SuperUser
20            $CLI $MAX_TIME $MAX_ROWS @steps);
21
22sub max_rows {
23    return $MAX_ROWS;
24}
25
26sub max_time {
27    return $MAX_TIME;
28}
29
30sub BEGIN {
31    $MAX_TIME = 5;
32    $MAX_ROWS = 100;
33
34    %functions = (
35        # standard routines
36        'core_upgrade_begin' => {
37            code => \&core_upgrade_begin,
38            priority => 1,
39        },
40        'core_fix_type' => {
41            code => \&core_fix_type,
42            priority => 2,
43        },
44        'core_add_column' => {
45            code => sub { shift->core_column_action('add', @_) },
46            priority => 3,
47        },
48        'core_drop_column' => {
49            code => sub { shift->core_column_action('drop', @_) },
50            priority => 3,
51        },
52        'core_alter_column' => {
53            code => sub { shift->core_column_action('alter', @_) },
54            priority => 3,
55        },
56        'core_index_column' => {
57            code => sub { shift->core_column_action('index', @_) },
58            priority => 3.5,
59        },
60        'core_seed_database' => {
61            code => \&seed_database,
62            priority => 4,
63        },
64        'core_upgrade_end' => {
65            code => \&core_upgrade_end,
66            priority => 9,
67        },
68        'core_finish' => {
69            code => \&core_finish,
70            priority => 10,
71        },
72    );
73
74    %LegacyPerms = (
75        # System-wide permissions
76        #[ 2**0, 'administer', 'System Administrator', 2, 'system' ],
77        #[ 2**1, 'create_blog', 'Create Blogs', 2, 'system' ],
78        #[ 2**2, 'view_log', 'View System Activity Log', 2, 'system' ],
79        #[ 2**3, 'manage_plugins', 'Manage Plugins', 'system' ],
80
81        # Blog-specific permissions:
82        # The order here is the same order they are presented on the
83        # role definition screen.
84        2**0 => 'comment',# 'Add Comments', 1, 'blog'],
85        2**12 => 'administer_blog',# 'Blog Administrator', 1, 'blog'],
86        2**6 => 'edit_config',# 'Configure Blog', 1, 'blog'],
87        2**3 => 'edit_all_posts',# 'Edit All Entries', 1, 'blog'],
88        2**4 => 'edit_templates',# 'Manage Templates', 1, 'blog'],
89        2**2 => 'upload',# 'Upload File', 1, 'blog'],
90        2**1 => 'post',# 'Create Entry', 1, 'blog'],
91        2**16 => 'edit_assets',# 'Manage Assets', 1, 'blog'],
92        2**15 => 'save_image_defaults',# 'Save Image Defaults', 1, 'blog'],
93        2**9 => 'edit_categories',# 'Add/Manage Categories', 1, 'blog'],
94        2**14 => 'edit_tags',# 'Manage Tags', 1, 'blog'],
95        2**10 => 'edit_notifications',# 'Manage Notification List', 1, 'blog'],
96        2**8 => 'send_notifications',# 'Send Notifications', 1, 'blog'],
97        2**13 => 'view_blog_log',# 'View Activity Log', 1, 'blog'],
98        #[ 2**17, 'publish_post', 'Publish Post', 1, 'blog'],
99        #[ 2**18, 'manage_feedback', 'Manage Feedback', 1, 'blog'],
100        #[ 2**19, 'set_publish_paths', 'Set Publishing Paths', 1, 'blog'],
101        #[ 2**20, 'manage_pages', 'Manage Pages', 1, 'blog'],
102        # 2**5 == 32 is deprecated; reserved for future use
103        2**7 => 'rebuild',# 'Rebuild Files', 1, 'blog'],
104        # Not a real permission but a denial thereeof; unlisted because it
105        # has no label.
106        2**11 => 'not_comment',# '', 1, 'blog'],
107    );
108}
109
110sub init {
111    my $pkg = shift;
112    unless (%classes) {
113        my $types = MT->registry('object_types');
114        foreach my $type (keys %$types) {
115            $classes{$type} = $types->{$type};
116        }
117    }
118
119    my $fns = MT::Component->registry('upgrade_functions') || [];
120    foreach my $fn_set (@$fns) {
121        %functions = ( %functions, %{ $fn_set } );
122    }
123
124    # Load versioned MT::Upgrade functions, ie MT::Upgrade::v3
125    # Only load the ones that will be required for upgrading
126    # from the source SchemaVersion
127    my $from = int( MT->config->SchemaVersion || 0 );
128    $from = 1 if $from < 1;
129    my $to = int( MT->version_number );
130    for (my $i = $from; $i <= $to; $i++) {
131        my $vpkg = "${pkg}::v$i";
132        eval "# line " . __LINE__ . " " . __FILE__ . "\nrequire $vpkg; 1;" or die;
133        next if $@;
134        my $fn_set = $vpkg->upgrade_functions();
135        %functions = ( %functions, %$fn_set )
136            if $fn_set && (ref($fn_set) eq 'HASH');
137    }
138}
139
140# Step execution...
141
142# iterate routines:
143#     no parameters, start with offset == 0
144#     offset parameter, pass thru
145#     if routine returns 0, routine is done
146#     if routine returns undef, routine failed
147#     if routine returns > 0, that's the new offset
148
149sub run_step {
150    my $self = shift;
151    my ($step) = @_;
152    my ($name, %param) = @$step;
153
154    if (my $fn = $functions{$name}) {
155        local $MT::CallbacksEnabled = 0;
156        if (my $cond = $fn->{condition}) {
157            $cond = MT->handler_to_coderef($cond);
158            next unless $cond->($self, %param);
159        }
160        my %update_params;
161        if ($fn->{updater}) {
162            %update_params = %{$fn->{updater}};
163            $fn->{code} ||= \&core_update_records;
164        }
165        my $code = $fn->{code} || $fn->{handler};
166        $code = MT->handler_to_coderef($code);
167        my $result = $code->($self, %param, %update_params, step => $name);
168        if (ref $result eq 'HASH') {
169            $param{$_} = $result->{$_} for keys %$result;
170            $result = 1;
171            $self->add_step($name, %param);
172        }
173        elsif ((defined $result) && ($result > 1)) {
174            $param{offset} = $result; $result = 1;
175            $self->add_step($name, %param);
176        }
177        return $result;
178    } else {
179        return $self->error($self->translate_escape("Invalid upgrade function: [_1].", $name));
180    }
181    0;
182}
183
184sub run_callbacks {
185    my $self = shift;
186    my ($cb, @param) = @_;
187    local $MT::CallbacksEnabled = 1;
188    MT->run_callbacks('MT::Upgrade::' . $cb, $self, @param)
189        or return $self->error(MT->callback_errstr);
190    1;
191}
192
193# Main "do" interface for controlling apparatus
194
195sub do_upgrade {
196    my $self = shift;
197    my (%opt) = @_;
198
199    $self->init;
200
201    my $harnessed = ref $opt{App} && (UNIVERSAL::can($opt{App}, 'add_step'));
202
203    local $App = $opt{App};
204    local $DryRun = $opt{DryRun};
205    local $SuperUser = $opt{SuperUser} || '';
206    local $CLI = $opt{CLI} || '';
207
208    @steps = ();
209    if ($opt{Install}) {
210        my %init_params = (%{$opt{User} || {}}, %{$opt{Blog} || {}});
211        $self->install_database(\%init_params);
212    } else {
213        $self->upgrade_database();
214    }
215
216    # no app is running the show, so we must!
217    if (!$harnessed) {
218        # set these limits very high since we're running unharnessed
219        $MAX_TIME = 10000000;
220        $MAX_ROWS = 300;
221        my $fn = \%MT::Upgrade::functions;
222        my @these_steps = @steps;
223
224        while (@these_steps) {
225            my $step = shift @these_steps;
226            @steps = ();
227            $self->run_step($step);
228            if (@steps) {
229                push @these_steps, @steps;
230                @these_steps = sort { $fn->{$a->[0]}->{priority} <=>
231                                      $fn->{$b->[0]}->{priority} } @these_steps;
232            }
233
234            # Reset the request to eliminate any caching that may be
235            # happening there (objects tend to cache into the request
236            # with the 'cache_property' method)
237            MT->request->reset;
238        }
239        return 1;
240    } else {
241        return \@steps;
242    }
243}
244
245sub upgrade_database {
246    my $self = shift;
247
248    my $config_schema_ver;
249    my $schema_ver;
250    if ($config_schema_ver = MT->instance->config('SchemaVersion')) {
251        my $needs_upgrade;
252        $needs_upgrade = 1 if $config_schema_ver < MT->schema_version;
253        if (!$needs_upgrade) {
254            foreach (@MT::Components) {
255                $needs_upgrade = 1 if $_->needs_upgrade;
256            }
257        }
258        return 1 unless $needs_upgrade;
259        $schema_ver = $config_schema_ver;
260    } else {
261        $schema_ver = $self->detect_schema_version;
262    }
263
264    # this will add steps to upgrade all tables that need it...
265    $self->add_step("core_upgrade_begin", from => $schema_ver);
266    $self->check_schema;
267    $self->add_step('core_upgrade_end', from => $schema_ver);
268    $self->add_step('core_finish');
269    1;
270}
271
272sub install_database {
273    my $self = shift;
274    my ($user) = @_;
275
276    $self->add_step("core_upgrade_begin");
277    # this will add steps to install all tables...
278    $self->check_schema;
279    # this will populate them...
280    $self->add_step('core_seed_database', %$user);
281    $self->add_step('core_upgrade_end');
282    $self->add_step('core_finish');
283    1;
284}
285
286sub check_schema {
287    my $self = shift;
288    my $class;
289    foreach my $type (keys %classes) {
290        $class = MT->model($type)
291            or return $self->error($self->translate_escape("Error loading class [_1].", $type));
292        $self->check_type($type);
293    }
294    1;
295}
296
297sub check_type {
298    my $self = shift;
299    my ($type) = @_;
300
301    my $class = MT->model($type);
302
303    # handle schema updates for meta table
304    if ($class->meta_pkg) {
305        $self->check_type($type . ':meta');
306    }
307
308    if (my $result = $self->type_diff($type)) {
309        if ($result->{fix}) {
310            $self->add_step('core_fix_type', type => $type);
311        } else {
312            $self->add_step('core_add_column', type => $type)
313                if $result->{add};
314            $self->add_step('core_alter_column', type => $type)
315                if $result->{alter};
316            $self->add_step('core_drop_column', type => $type)
317                if $result->{drop};
318            $self->add_step('core_index_column', type => $type)
319                if $result->{index};
320        }
321    }
322
323    1;
324}
325
326sub type_diff {
327    my $self = shift;
328    my ($type) = @_;
329
330    my $class = MT->model($type) or return;
331
332    my $table = $class->datasource;
333    my $defs = $class->column_defs;
334
335    my $ddl = $class->driver->dbd->ddl_class;
336    my $db_defs = $ddl->column_defs($class);
337
338    my $class_idx_defs = $class->index_defs;
339    my $db_idx_defs = $ddl->index_defs($class);
340
341    # now, compare $defs and $db_defs;
342    # here are the scenarios
343    #   1. we find something in $defs that isn't in $db_defs
344    #      -- column should be inserted. this may trigger a process
345    #   2. we find something in $db_defs that isn't in $defs
346    #      -- this is a-ok. user may have added a column.
347    #   3. we find a difference between $defs and $db_defs for a field
348    #      a. type differs; this may trigger a process
349    #      b. type is same, but null property differs; this may
350    #         trigger a process
351    #      c. type is same, but size differs; this may trigger a process
352    #      d. key differs
353    #      e. auto differs (auto-increment)
354    #   4. table doesn't exist and must be created
355
356    my $fix_class;
357    $fix_class = 1 unless defined $db_defs;
358
359    # we're only scanning defined columns; we don't care about
360    # columns that are unique to the table.
361    my (@cols_to_add, @cols_to_alter, @cols_to_drop, @cols_to_index);
362
363    if (!$fix_class) {
364        my @def_cols = keys %$defs;
365
366        foreach my $col (@def_cols) {
367            my $col_def = $defs->{$col};
368            next if !defined $col_def;
369
370            $col_def->{name} = $col;
371
372            my $db_def = $db_defs->{$col};
373
374            if (!$db_def) {
375                # column is missing altogether; we're going to have to add it
376                push @cols_to_add, $col;
377            } else {
378                if (($col_def->{type} eq 'string')
379                 && ($db_def->{type} eq 'string')
380                 && ($col_def->{size} != $db_def->{size})) {
381                    push @cols_to_alter, $col;
382                } elsif ($ddl->type2db($col_def)
383                      ne $ddl->type2db($db_def)) {
384                    push @cols_to_alter, $col;
385                } elsif (($col_def->{not_null} || 0) != ($db_def->{not_null} || 0)) {
386                    push @cols_to_alter, $col;
387                }
388            }
389        }
390
391        foreach my $key (keys %$class_idx_defs) {
392            my $db_idx_def = $db_idx_defs->{$key};
393            if (!$db_idx_def) {
394                push @cols_to_index, $key;
395                next;
396            }
397            # if there is a mismatch in definition, add it to index
398            my $class_idx_def = $class_idx_defs->{$key};
399            if (ref($class_idx_def)) {
400                if (!ref $db_idx_def) {
401                    push @cols_to_index, $key;
402                }
403                else {
404                    my $db_cols;
405                    if (exists $db_idx_def->{columns}) {
406                        $db_cols = join ',', @{ $db_idx_def->{columns} };
407                    }
408                    else {
409                        $db_cols = $key;
410                    }
411                    my $class_cols;
412                    if (exists $class_idx_def->{columns}) {
413                        $class_cols = join ',', @{ $class_idx_def->{columns} };
414                    }
415                    else {
416                        $class_cols = $key;
417                    }
418                    if ($db_cols ne $class_cols) {
419                        push @cols_to_index, $key;
420                    }
421                    else {
422                        if (($db_idx_def->{unique} || 0) != ($class_idx_def->{unique} || 0)) {
423                            push @cols_to_index, $key;
424                        }
425                    }
426                }
427            }
428            else {
429                if (ref $db_idx_def) {
430                    push @cols_to_index, $key;
431                }
432            }
433        }
434    }
435
436    if ($fix_class || @cols_to_add || @cols_to_alter || @cols_to_drop || @cols_to_index) {
437        my %param;
438        $param{drop} = \@cols_to_drop if @cols_to_drop;
439        $param{add} = \@cols_to_add if @cols_to_add;
440        $param{alter} = \@cols_to_alter if @cols_to_alter;
441        $param{fix} = $fix_class;
442        $param{index} = \@cols_to_index if @cols_to_index;
443        if ((@cols_to_add && !$ddl->can_add_column) ||
444            (@cols_to_alter && !$ddl->can_alter_column) || 
445            (@cols_to_drop && !$ddl->can_drop_column)) {
446            $param{fix} = 1;
447        }
448        return \%param;
449    }
450    undef;
451}
452
453sub seed_database {
454    my $self = shift;
455    my (%param) = @_;
456    $self->run_callbacks('seed_database', %param);
457    return 1;
458}
459
460###  Upgrade triggers
461
462# we don't need these yet, but it makes me feel good to have them around
463
464# 'pre' triggers should execute quickly. 'post' triggers can add steps
465# if they require processing that will take time to complete.
466
467sub pre_upgrade_class {
468    return 1;
469}
470
471sub post_upgrade_class {
472    my $self = shift;
473    my ($class) = @_;
474
475    # Special case for handling upgrade process for old "meta" column
476    # storage to new narrow tables; some database drivers cannot
477    # add new columns without recreating the table, so it's necessary
478    # to prioritize migration of meta column data before the schema
479    # for that class is updated and the meta column winds up getting
480    # dropped as a result.
481    return unless MT->config->SchemaVersion;
482    if (MT->config->SchemaVersion < 4.0057) {
483        return 1 unless $class =~ m/::Meta$/;
484
485        my $pc = $class;
486        $pc =~ s/::Meta$//;
487
488        my $type = $pc->datasource;
489        # 'page' instead of 'entry', for instance
490        $type = $pc->class_type || $type if $pc->can('class_type');
491
492        my %step_param = ( type => $type );
493        $step_param{plugindata} = 1 if $type eq 'category';
494        $step_param{meta_column} = $pc->properties->{meta_column}
495            if $pc->properties->{meta_column};
496        $self->add_step('core_upgrade_meta_for_table', %step_param);
497    }
498
499    return 1;
500}
501
502sub pre_alter_column { 1 }
503sub post_alter_column { 1 }
504sub pre_drop_column { 1 }
505sub post_drop_column { 1 }
506sub pre_add_column { 1 }
507sub pre_index_column { 1 }
508sub post_index_column { 1 }
509sub pre_schema_upgrade { 1 }
510
511# issued last, after all table creation...
512
513sub post_schema_upgrade {
514    my $self = shift;
515    my ($from) = @_;
516
517    my $plugin_ver = MT->config('PluginSchemaVersion') || {};
518    $plugin_ver->{'core'} = $from;
519
520    # run any functions that define a version_limit and where the schema we're
521    # upgrading from is below that limit.
522    foreach my $fn (keys %functions) {
523        my $save_from = $from;
524        {
525            my $func = $functions{$fn};
526
527            if ($func->{plugin} && (UNIVERSAL::isa($func->{plugin}, 'MT::Component'))) {
528                my $id = $func->{plugin}->id;
529                $from = $plugin_ver->{$id};
530            }
531            if ($func->{version_limit}
532                && (defined $from)
533                && ($from < $func->{version_limit})) {
534                $self->add_step($fn, from => $from);
535            }
536            elsif ($func
537                && !exists($func->{version_limit})
538                && !defined($from)) {
539                $self->add_step($fn);
540            }
541        }
542        $from = $save_from;
543    }
544
545    1;
546}
547
548sub pre_create_table {
549    my $self = shift;
550    my ($class) = @_;
551    $class->driver->dbd->ddl_class->drop_sequence($class);
552}
553
554sub post_create_table {
555    my $self = shift;
556    my ($class) = @_;
557
558    $class->driver->dbd->ddl_class->create_sequence($class);
559
560    if (!$Installing) {
561        foreach (keys %functions) {
562            my $func = $functions{$_};
563            next unless $func->{on_class};
564            $self->add_step($_) if $func->{on_class} eq $class;
565        }
566    }
567
568    1;
569}
570
571# Note that this trigger only fires on BerkeleyDB for columns
572# that are non-null or indexed.
573
574sub post_add_column {
575    my $self = shift;
576    my ($class, $col_defs) = @_;
577
578    if (!$Installing) {
579        my %cols = map { $_ => 1 } @$col_defs;
580        foreach (keys %functions) {
581            my $func = $functions{$_};
582            next unless $func->{on_field};
583            if ($func->{on_field} =~ m/^\Q$class\E->(.*)/) {
584                $self->add_step($_) if $cols{$1};
585            }
586        }
587    }
588    1;
589}
590
591# Passthru routines-- passing to calling application...
592
593sub progress {
594    my $self = shift;
595    $App->progress(@_) if $App;
596}
597
598sub translate_escape {
599    my $self = shift;
600    my $trans = MT->translate(@_);
601    return $trans if $CLI;
602    $trans = MT::I18N::encode_text($trans, undef, 'utf-8');
603    return MT::Util::escape_unicode($trans);
604}
605
606sub error {
607    my $self = shift;
608    my ($msg) = @_;
609    $App->error(@_) if $App;
610    return undef;
611}
612
613sub add_step {
614    my $self = shift;
615    if ($App && (ref $App)) {
616        $App->add_step(@_);
617    } else {
618        push @steps, [ @_ ];
619    }
620}
621
622# Misc utilities.
623
624sub detect_schema_version {
625    my $self = shift;
626
627    require MT::Object;
628    my $driver = MT::Object->driver;
629
630    require MT::Config;
631    if ($driver->table_exists('MT::Config')) {
632        return 3.2;
633    }
634
635    require MT::Template;
636    my $dyn_error_template = 
637        MT::Template->exist({type => 'dynamic_error'});
638    if ($dyn_error_template) {
639        return 3.1;
640    }
641
642    my $comment_pending_template =
643        MT::Template->exist({type => 'comment_pending'});
644    if ($comment_pending_template) {
645        return 3.0;
646    }
647
648    require MT::TemplateMap;
649    if ($driver->table_exists('MT::TemplateMap')) {
650        return 2.0;
651    }
652
653    1.0;
654}
655
656# A note about upgrade routines:
657#
658# They should all be 'safe' to execute, regardless of the
659# active schema. In other words, running them twice in a row
660# should not cause any errors or break the schema.
661
662sub core_fix_type {
663    my $self = shift;
664    my (%param) = @_;
665
666    my $type = $param{type};
667    my $class = MT->model($type);
668
669    my $result = $self->type_diff($type);
670    return 1 unless $result;
671    return 1 unless $result->{fix};
672
673    my $alter = $result->{alter};
674    my $add = $result->{add};
675    my $drop = $result->{drop};
676    my $index = $result->{index};
677
678    my $driver = $class->driver;
679    my $ddl = $driver->dbd->ddl_class;
680    my @stmts;
681    push @stmts, sub { $self->pre_upgrade_class($class) };
682    push @stmts, $ddl->upgrade_begin($class);
683    push @stmts, sub { $self->pre_create_table($class) };
684    push @stmts, sub { $self->pre_add_column($class, $add) } if $add;
685    push @stmts, sub { $self->pre_alter_column($class, $alter) } if $alter;
686    push @stmts, sub { $self->pre_drop_column($class, $drop) } if $drop;
687    push @stmts, sub { $self->pre_index_column($class, $index) } if $index;
688    push @stmts, $ddl->fix_class($class);
689    push @stmts, sub { $self->post_create_table($class) };
690    push @stmts, sub { $self->post_add_column($class, $add) } if $add;
691    push @stmts, sub { $self->post_alter_column($class, $alter) } if $alter;
692    push @stmts, sub { $self->post_drop_column($class, $drop) } if $drop;
693    push @stmts, sub { $self->post_index_column($class, $index) } if $index;
694    push @stmts, $ddl->upgrade_end($class);
695    push @stmts, sub { $self->post_upgrade_class($class) };
696    $self->run_statements($class, @stmts);
697}
698
699sub core_column_action {
700    my $self = shift;
701    my ($action, %param) = @_;
702
703    my $type = $param{type};
704    my $class = MT->model($type);
705    my $defs = $class->column_defs;
706
707    my $result = $self->type_diff($type);
708    return 1 unless $result;
709    my $columns = $result->{$action};
710    return 1 unless $columns;
711
712    my $pre_method = "pre_${action}_column";
713    my $post_method = "post_${action}_column";
714    my $method = "${action}_column";
715
716    my $driver = $class->driver;
717    my $ddl = $driver->dbd->ddl_class;
718    my @stmts;
719    push @stmts, sub { $self->pre_upgrade_class($class) };
720    push @stmts, $ddl->upgrade_begin($class);
721    push @stmts, sub { $self->$pre_method($class, $columns) };
722    push @stmts, $ddl->$method($class, $_) foreach @$columns;
723    push @stmts, sub { $self->$post_method($class, $columns) };
724    push @stmts, $ddl->upgrade_end($class);
725    push @stmts, sub { $self->post_upgrade_class($class) };
726    $self->run_statements($class, @stmts);
727}
728
729sub run_statements {
730    my $self = shift;
731    my ($class, @stmts) = @_;
732
733    my $driver = $class->driver;
734    my $defs = $class->column_defs;
735    my $dbh = $driver->rw_handle;
736    my $mt = MT->instance;
737
738    my $updated = 0;
739    if (@stmts) {
740        $self->progress($self->translate_escape("Upgrading table for [_1] records...", $class->can('class_label') ? $class->class_label : $class));
741        eval {
742            foreach my $stmt (@stmts) {
743                if (ref $stmt eq 'CODE') {
744                    $stmt->() if !$DryRun;
745                } else {
746                    if ($dbh && !$DryRun) {
747                        my $err;
748                        $dbh->do($stmt) or $err = $dbh->errstr;
749                        if ($err) {
750                            # ignore drop errors; the table/sequence/constraint
751                            # didn't exist
752                            if (($stmt !~ m/^drop /i) && ($stmt !~ m/DROP CONSTRAINT /i)) {
753                                die "failed to execute statement $stmt: $err";
754                            }
755                        }
756                    } elsif ($dbh && $DryRun) {
757                        $self->run_callbacks('SQL', $stmt);
758                    }
759                }
760                $updated = 1;
761            }
762        };
763        if ($@) {
764            return $self->error($@);
765        }
766    }
767    $updated;
768}
769
770sub core_upgrade_begin {
771    my $self = shift;
772    my (%param) = @_;
773    my $from_schema = $param{from};
774    if ($from_schema) {
775        my $cur_schema = MT->schema_version;
776        $self->progress($self->translate_escape("Upgrading database from version [_1].", $from_schema)) if $from_schema < $cur_schema;
777        $self->pre_schema_upgrade($from_schema);
778    }
779    $self->run_callbacks('upgrade_begin', %param);
780    return 1;
781}
782
783sub core_upgrade_end {
784    my $self = shift;
785    my (%param) = @_;
786
787    my $from_schema = $param{from};
788    if ($from_schema) {
789        $self->post_schema_upgrade($from_schema);
790    }
791    $self->run_callbacks('upgrade_end', %param);
792    return 1;
793}
794
795sub core_finish {
796    my $self = shift;
797
798    my $user;
799    if ((ref $App) && ($App->{author})) {
800        $user = $App->{author};
801    }
802
803    my $cfg = MT->config;
804    my $cur_schema = MT->instance->schema_version;
805    my $old_schema = $cfg->SchemaVersion || 0;
806    if ($cur_schema > $old_schema) {
807        $self->progress($self->translate_escape("Database has been upgraded to version [_1].", $cur_schema)) ;
808        if ($user && !$DryRun) {
809            MT->log(MT->translate("User '[_1]' upgraded database to version [_2]", $user->name, $cur_schema));
810        }
811        $cfg->SchemaVersion( $cur_schema, 1 );
812    }
813
814    my $plugin_schema = $cfg->PluginSchemaVersion || {};
815    foreach my $plugin (@MT::Components) {
816        my $ver = $plugin->schema_version;
817        next unless $ver;
818        next if $plugin->id eq 'core';
819        my $old_plugin_schema = $plugin_schema->{$plugin->id} || 0;
820        if ($old_plugin_schema && ($ver > $old_plugin_schema)) {
821            $self->progress($self->translate_escape("Plugin '[_1]' upgraded successfully to version [_2] (schema version [_3]).", $plugin->label, $plugin->version || '-', $ver));
822            if ($user && !$DryRun) {
823                MT->log(MT->translate("User '[_1]' upgraded plugin '[_2]' to version [_3] (schema version [_4]).", $user->name, $plugin->label, $plugin->version || '-', $ver));
824            }
825        } elsif ($ver && !$old_plugin_schema) {
826            $self->progress($self->translate_escape("Plugin '[_1]' installed successfully.", $plugin->label));
827            if ($user && !$DryRun) {
828                MT->log(MT->translate("User '[_1]' installed plugin '[_2]', version [_3] (schema version [_4]).", $user->name, $plugin->label, $plugin->version || '-', $ver));
829            }
830        }
831        $plugin_schema->{$plugin->id} = $ver;
832    }
833    if (keys %$plugin_schema) {
834        $cfg->PluginSchemaVersion($plugin_schema, 1);
835    }
836
837    my $cur_version = MT->version_number;
838    if ( !defined($cfg->MTVersion) || ( $cur_version > $cfg->MTVersion ) ) {
839        $cfg->MTVersion( $cur_version, 1 );
840    }
841    $cfg->save_config unless $DryRun;
842
843    # do one last thing....
844    if ((ref $App) && ($App->can('finish'))) {
845        $App->finish();
846    }
847
848    1;
849}
850
851sub core_update_entry_counts {
852    my $self = shift;
853    my (%param) = @_;
854
855    my $class = MT->model('entry');
856    return $self->error($self->translate_escape("Error loading class: [_1].", $param{type}))
857        unless $class;
858
859    my $msg = $self->translate_escape("Assigning entry comment and TrackBack counts...");
860    my $offset = $param{offset} || 0;
861    my $count = $param{count};
862    if (!$count) {
863        $count = $class->count({ class => '*' });
864    }
865    return unless $count;
866    if ($offset) {
867        $self->progress(sprintf("$msg (%d%%)", ($offset/$count*100)), $param{step});
868    } else {
869        $self->progress($msg, $param{step});
870    }
871
872    my $continue = 0;
873    my $driver = $class->driver;
874
875    my $iter = $class->load_iter({ class => '*' }, { offset => $offset, limit => $MAX_ROWS+1 });
876    my $start = time;
877    my ( %touched, %c, %tb );
878    my $rows = 0;
879    while (my $e = $iter->()) {
880        $rows++;
881        $c{$e->id} = $e;
882        if (my $tb = $e->trackback) {
883            $tb{$tb->id} = $e;
884        }
885        $continue = 1, last if scalar $rows == $MAX_ROWS;
886    }
887    if ( $continue ) {
888        $iter->end;
889        $offset += $rows;
890    }
891
892    # now gather counts -- comments
893    if (my $grp_iter = MT::Comment->count_group_by({
894        visible => 1,
895        entry_id => [ keys %c ],
896    }, {
897        group => ['entry_id'],
898    })) {
899        while (my ($count, $id) = $grp_iter->()) {
900            my $e = $c{$id} or next;
901            if ((!defined $e->comment_count) || (($e->comment_count || 0) != $count)) {
902                $e->comment_count($count);
903                $touched{$e->id} = $e;
904            }
905        }
906    }
907
908    # pings
909    if ( %tb ) {
910        if (my $grp_iter = MT::TBPing->count_group_by({
911            visible => 1,
912            tb_id => [ keys %tb ],
913        }, {
914            group => ['tb_id'],
915        })) {
916            while (my ($count, $id) = $grp_iter->()) {
917                my $e = $tb{$id} or next;
918                if ((!defined $e->ping_count) || (($e->ping_count || 0) != $count)) {
919                    $e->ping_count($count);
920                    $touched{$e->id} = $e;
921                }
922            }
923        }
924    }
925
926    foreach my $e (values %touched) {
927        $e->save;
928    }
929
930    if ($continue) {
931        return { offset => $offset, count => $count };
932    } else {
933        $self->progress("$msg (100%)", $param{step});
934    }
935    1;
936}
937
938sub core_update_records {
939    my $self = shift;
940    my (%param) = @_;
941
942    my $class = MT->model($param{type});
943    return $self->error($self->translate_escape("Error loading class: [_1].", $param{type}))
944        unless $class;
945
946    my $msg;
947    my $class_label = ($class->can('class_label') ? $class->class_label : $class);
948    if ($param{label}) {
949        $msg = $param{label};
950        if (ref $msg eq 'CODE') {
951            $msg = $msg->($class_label);
952        }
953        $msg = $self->translate_escape($msg);
954    } else {
955        $msg = $self->translate_escape($param{message} || "Updating [_1] records...", $class_label);
956    }
957    my $offset = $param{offset};
958    my $count = $param{count};
959    if (!$count) {
960        $count = $class->count;
961    }
962    return unless $count;
963    if ($offset) {
964        $self->progress(sprintf("$msg (%d%%)", ($offset/$count*100)), $param{step});
965    } else {
966        $self->progress($msg, $param{step});
967    }
968
969    my $cond = MT->handler_to_coderef($param{condition});
970    my $code = MT->handler_to_coderef($param{code});
971    my $sql = $param{sql};
972
973    my $continue = 0;
974    my $driver = $class->driver;
975
976    if ($sql && $DryRun) {
977        $self->run_callbacks('SQL', $sql);
978    }
979    return 1 if $DryRun;
980
981    if (!$sql || !$driver->sql($sql)) {
982        my $iter= $class->load_iter(undef, { offset => $offset, limit => $MAX_ROWS+1 });
983        my $start = time;
984        my @list;
985        while (my $obj = $iter->()) {
986            push @list, $obj;
987            $continue = 1, last if scalar @list == $MAX_ROWS;
988        }
989        $iter->end if $continue;
990        for my $obj (@list) {
991            $offset++;
992            if ($cond) {
993                next unless $cond->($obj, %param);
994            }
995            $code->($obj);
996            $obj->save()
997                or return $self->error($self->translate_escape("Error saving [_1] record # [_3]: [_2]...", $class_label, $obj->errstr, $obj->id));
998            $continue = 1, last if time > $start + $MAX_TIME;
999        }
1000    }
1001    if ($continue) {
1002        return { offset => $offset, count => $count };
1003    } else {
1004        $self->progress("$msg (100%)", $param{step});
1005    }
1006    1;
1007}
1008
10091;
1010__END__
1011
1012=head1 NAME
1013
1014MT::Upgrade - MT class for managing system upgrades.
1015
1016=head1 SYNOPSIS
1017
1018    MT::Upgrade->do_upgrade(Install => 1);
1019
1020=head1 DESCRIPTION
1021
1022This module is responsible for handling the upgrade or installation of
1023an MT database. The framework is flexible enough for third party plugins
1024to use as well to manage their own schema (please refer to the documentation
1025in L<MT::Plugin> for more information on this).
1026
1027=head1 METHODS
1028
1029=head2 MT::Upgrade-E<gt>do_upgrade
1030
1031The main worker method for this module is I<do_upgrade>. It accepts a
1032handful of arguments, which are:
1033
1034=over 4
1035
1036=item * Install
1037
1038Specify a value of '1' to assume a new installation along with an operation
1039to install a blog and initial user.
1040
1041=item * App
1042
1043A package name or app object that can service the following methods:
1044
1045=over 4
1046
1047=item * progress($package, $message)
1048
1049Called during the upgrade operation to provide feedback with respect to
1050the operations the upgrade process is running.
1051
1052=item * error($package, $message)
1053
1054Called during the upgrade operation to communicate an error that has
1055occurred.
1056
1057=item * translate_escape
1058
1059Call this method to translate messages and phrases which are to appear
1060on the progress screen.  DO NOT use this method to messages and phrases
1061which directly are stored in database.  Use MT->translate for the purpose.
1062
1063=back
1064
1065=item * CLI
1066
1067Specified (set to '1') when invoked from a command line tool. This prevents
1068encoding response messages in the configured PublishCharset for the
1069installation.
1070
1071=item * SuperUser
1072
1073If upgrading from the command line, and running on a pre-MT 3.2 database,
1074set this to an existing author ID that should be upgrade to system
1075administrator status.
1076
1077=item * DryRun
1078
1079Specified (set to '1') to examine the database for installation/upgrade
1080needs but not actually make any physical changes to the database. This will
1081issue all the upgrade progress messages without doing the upgrade itself.
1082
1083=back
1084
1085=head2 MT::Upgrade->add_step( $function[, %params ] )
1086
1087Adds an additional upgrade function to the upgrade pipeline. The
1088parameters given will be given to the upgrade function when invoked.
1089Note that all values in the C<%params> hash should be simple scalar
1090values, as they have to be represented in JSON notation.
1091
1092=head2 MT::Upgrade->check_schema()
1093
1094Run during the upgrade process to verify all object types are
1095up-to-date. Calls the L<check_type> method which does the work
1096for an individual object type. Returns '1' for success, or
1097undef for failure.
1098
1099=head2 MT::Upgrade->check_type($type)
1100
1101Issues schema checks for the specified C<$type>. If schema differences
1102exist, upgrade steps are added to bring the object type up to date.
1103
1104=head2 MT::Upgrade->core_column_action($action, %params)
1105
1106Upgrade function to process an object type in a specific
1107way. C<$action> may be one of C<add>, C<drop>, C<alter>, C<index>.
1108C<%params> should contain a C<type> parameter identifying the object
1109type to process.
1110
1111=head2 MT::Upgrade->core_create_config_table
1112
1113Upgrade function to handle the creation of the initial
1114L<MT::Config> object.
1115
1116=head2 MT::Upgrade->core_create_template_maps
1117
1118Upgrade function to handle the creation of L<MT::TemplateMap> records
1119for upgrading pre-2.0 MT schemas.
1120
1121=head2 MT::Upgrade->core_drop_meta_for_table
1122
1123Upgrade function to handle the removal of "meta" blob columns for
1124pre-MT 4.2 schemas.
1125
1126=head2 MT::Upgrade->core_finish
1127
1128Upgrade function that finalizes an upgrade operation. This routine
1129is always run at the end of the upgrade process.
1130
1131=head2 MT::Upgrade->core_fix_type
1132
1133Upgrade function that handles a table "overhaul", where it is necessary
1134to create a new temporary table, drop the existing one, then rebuild it
1135from scratch. This is necessary when an object driver (SQLite, for instance)
1136does not support a kind of table manipulation that is required to upgrade
1137it.
1138
1139=head2 MT::Upgrade->core_populate_author_auth_type
1140
1141Upgrade function to handle the assignment of the authentication type
1142(specifically, the 'auth_type' column) for upgraded L<MT::Author> records.
1143
1144=head2 MT::Upgrade->core_remove_unique_constraints
1145
1146Upgrade function that removes some old table constraints for pre-3.2
1147MT schemas.
1148
1149=head2 MT::Upgrade->core_set_enable_archive_paths
1150
1151Upgrade function that enables the C<EnableArchivePaths> configuration
1152setting, if the existing schema version is 3.2 or earlier (preserves
1153'archive path', 'archive url' blog settings fields).
1154
1155=head1 CALLBACKS
1156
1157The upgrade module defines the following MT callbacks:
1158
1159=over 4
1160
1161=item * MT::Upgrade::SQL
1162
1163Called with each SQL statement that is executed against the database
1164as part of the upgrade process. The parameters passed to this callback are:
1165
1166    $callback, $upgrade_app, $sql_statement
1167
1168The first parameter is an L<MT::Callback> object. C<$upgrade_app> is a
1169package name or L<MT::App> object used to drive the upgrade process.
1170C<$sql_statement> is the actual SQL query that is about to be executed
1171against the database.
1172
1173=back
1174
1175=head1 UPGRADE FUNCTIONS
1176
1177The bulk of this module consists of Movable Type upgrade operations.
1178These are declared as upgrade functions, and are registered in the
1179package variabled '%functions'. (Note: the word 'function' here is
1180not meant to describe a Perl subroutine.)
1181
1182Some functions are invoked to manage the upgrade process from start
1183to finish ('core_upgrade_begin' for instance, which merely displays
1184a progress message to the calling application). The rest handle
1185schema and data transformation from one version of the MT schema to
1186another.
1187
1188Schema translation itself is handled by Movable Type automatically.
1189MT is able to check the physical schema represenation in the database
1190and compare it with the schema as defined by the L<MT::Object>-descended
1191package. If a new property is added to the L<MT::Blog> package, the
1192upgrade process sees that has happened and can issue the actual
1193'alter table' SQL statement necessary to add it to the database. The
1194'core_fix_type' function is responsible for examining a particular
1195table used by a class like L<MT::Blog> and will append additional
1196upgrade steps ('core_add_column', 'core_alter_column') that it finds
1197necessary to the upgrade workflow.
1198
1199Following the schema translation operations, the data transformation
1200functions would be used to manipulate the data as necessary from
1201an older schema to the current one. For instance, the
1202'core_create_placements' upgrade function was written to upgrade
1203really old MT schemas from the pre-2.0 release to the current schema.
1204The upgrade function is registered like this:
1205
1206    $MT::Upgrade::functions{core_create_placements} = {
1207        version_limit => 2.0,
1208        code          => \&core_update_records,
1209        priority      => 9.1,
1210        updater       => {
1211            class     => 'MT::Entry',
1212            message   => 'Creating entry category placements...',
1213            condition => sub { $_[0]->category_id },
1214            code      => sub {
1215                require MT::Placement;
1216                my $entry = shift;
1217                my $existing = MT::Placement->load({ entry_id => $entry->id,
1218                    category_id => $entry->category_id });
1219                if (!$existing) {
1220                    my $place = MT::Placement->new;
1221                    $place->entry_id($entry->id);
1222                    $place->blog_id($entry->blog_id);
1223                    $place->category_id($entry->category_id);
1224                    $place->is_primary(1);
1225                    $place->save;
1226                }
1227                $entry->category_id(0);
1228            }
1229        }
1230    };
1231
1232With MT version 2.0, the L<MT::Placement> class was introduced and
1233immediately deprecated the use of MT::Entry-E<gt>category as a result.
1234To facilitate upgrading the existing L<MT::Entry> objects this upgrade
1235function is declared such that:
1236
1237=over 4
1238
1239=item * It is limited to only run for MT schemas older than version 2.0 (the version_limit element handles this).
1240
1241=item * It operates on L<MT::Entry> objects (updater-E<gt>class element
1242declares that).
1243
1244=item * It tells the user what is happening (updater-E<gt>message).
1245
1246=item * It excludes any L<MT::Entry> objects that do not have a category_id element (updater-E<gt>condition).
1247
1248=item * It checks for an existing L<MT::Placement> relationship; if not
1249present, it creates one (updater-E<gt>code).
1250
1251=item * It empties out the category_id member of the L<MT::Entry> object
1252being upgrade to prevent it from being processed in the future
1253(updater-E<gt>code).
1254
1255=back
1256
1257For plugins, upgrade functions are assignable in the plugin registration
1258hash as documented in L<MT::Plugin>. You may also return a hashref of
1259upgrade functions from the plugin using the MT::Plugin::upgrade_functions
1260subroutine.
1261
1262Let's look at the anatomy of an upgrade function declaration:
1263
1264=over 4
1265
1266=item * version_limit (optional)
1267
1268The version_limit property allows you to declare that this upgrade
1269operation is only applicable to MT B<schema> versions below the version
1270specified.
1271
1272To register an upgrade function that is only applied to releases prior
1273to the current one, specify the current schema version as the version
1274limit. This will allow the upgrade function to run for any prior releases
1275but prevent it from running in subsequent releases.
1276
1277B<NOTE>: If you are declaring a B<plugin> upgrade function, this version
1278limit is compared with your plugin's schema version, not the Movable Type
1279schema version.
1280
1281=item * priority (optional)
1282
1283If your upgrade operation is dependent on another being done already,
1284it is possible to order them using the priority value. A lower value
1285means a higher priority.
1286
1287=item * condition (optional)
1288
1289This is a coderef parameter. If specified, it should return a true or
1290false value that determines whether the upgrade step is actually to run
1291or not.
1292
1293When called, it is given the parameters normally passed to an upgrade
1294operation (see the 'code' parameter documentation).
1295
1296=item * on_field (optional)
1297
1298If specified, this upgrade function is triggered upon the creation of
1299the field identified by this element. For instance,
1300
1301    on_field => 'MT::Foo->bar'
1302
1303This would specify that the upgrade step is only to run when the 'bar'
1304column is being added to the table that stores data for the MT::Foo
1305package.
1306
1307=item * code
1308
1309This coderef parameter is the declared handler for the upgrade
1310function. It is responsible for doing the upgrade task itself. For
1311quick operations, it is fine to do all of your work within this
1312subroutine. However, to faciliate large databases, it is important
1313to do that work in manageable portions so it doesn't time-out by
1314the web server or browser client.
1315
1316To facilitate an iterative process for your upgrade function, the
1317upgrade routine itself can yield a return value to signal the
1318upgrade process on how to proceed:
1319
1320=over 4
1321
1322=item * 0
1323
1324The upgrade function completed successfully.
1325
1326=item * undef
1327
1328upgrade routine failed with error. The error should be placed using the
1329MT::Upgrade-E<gt>error method.
1330
1331=item * E<gt> 0
1332
1333More work to do; the return value is the 'offset' parameter
1334to pass on the next invocation of the upgrade function.
1335
1336=back
1337
1338Due to the complexity of handling this kind of staged operation,
1339you will most likely want to use the prebuilt
1340'MT::Upgrade::core_update_records' routine to do most of your upgrade
1341operations that handle some or all records of a given package.
1342
1343If using the 'core_update_records' routine, you should also specify
1344an 'updater' parameter for your upgrade function.
1345
1346=item * updater
1347
1348This parameter is only used if you've specified the 'core_update_records'
1349routine (from the L<MT::Upgrade> package itself) for the 'code' element of
1350your upgrade function.
1351
1352    code => \&MT::Upgrade::core_update_records,
1353    updater => {
1354        class => 'MT::Foo',
1355        message => 'Updating Foo bars...',
1356        code => sub {
1357            my $foo = shift;
1358            $foo->bar(1);
1359        },
1360        condition => sub {
1361            my $foo = shift;
1362            !defined $foo->bar;
1363        },
1364        sql => 'update mt_foo set foo_bar = 1 where foo_bar is null'
1365    }
1366
1367This updater declaration is going to process all MT::Foo objects that
1368are available, setting the 'bar' property to 1 if it hasn't been assigned
1369a value already.
1370
1371Here's an overview of an 'updater' element:
1372
1373=over 4
1374
1375=item * class (required)
1376
1377The L<MT::Object>-descendant class to be processed.
1378
1379=item * code (required)
1380
1381A coderef to execute for B<each> record of the table. The parameter to
1382this routine is the object being processed. Following the call to your
1383subroutine, the object is saved for you, so you don't have to save
1384the object yourself.
1385
1386=item * message (optional)
1387
1388The status message to display when running this upgrade operation.
1389
1390=item * condition (optional)
1391
1392A coderef to use to test whether the current object needs to be upgraded
1393or not. This routine should return true if it is to be processed; false
1394if not. It is given the object as a parameter.
1395
1396=item * sql (optional)
1397
1398If specified, and if MT is using a SQL-based database for storing data,
1399this SQL statement is issued instead of doing the Perl-based row-by-row
1400upgrade.
1401
1402    sql => 'update mt_foo set foo_bar=1 where foo_bar is null'
1403
1404You may also specify multiple SQL statements using an array:
1405
1406    sql => [
1407        'update mt_foo set foo_bar=1 where foo_bar is null',
1408        'update mt_foo set foo_baz=2 where foo_baz is null'
1409    ]
1410
1411B<WARNING>: The 'sql' property is only meant to be used for cases where you
1412can issue simple, cross-database SQL statements. It is not advised to
1413use any vendor-specific SQL syntax. So, if you can't do that, don't specify
1414the 'sql' element at all and instead use the 'code' element exclusively
1415to do the upgrade operation.
1416
1417=back
1418
1419=back
1420
1421The declarative style of upgrade functions make it possible for MT to
1422fix itself, upgrading from any older schema version to the current one.
1423Upgrade functions are selected through an introspection process, so any
1424given upgrade operation may run a different selection of upgrade functions.
1425As such, it is important that any upgrade functions be written with this
1426in mind. Here are some general best practices to use when writing them:
1427
1428=over 4
1429
1430=item * Make them fast.
1431
1432Use the 'sql' element for a 'core_update_records' type upgrade function
1433so that SQL-based databases can be upgraded in one pass.
1434
1435=item * Make them indepedent.
1436
1437Don't assume that any other upgrade operation will have run within the
1438same application request. The upgrade process can run them in most any
1439order and across multiple application requests. You do have a guarantee
1440that a higher priority upgrade function will be run prior to a lower-priority
1441upgrade function (ie, assigning a priority of 1 will ensure it will run
1442before one with a priority of 2).
1443
1444=item * Limit them as much as possible.
1445
1446Specify a version_limit so it only runs for the proper schemas. Use the
1447condition element to bypass objects or the upgrade step altogether when
1448possible.
1449
1450=item * Repeating an upgrade function should be safe.
1451
1452This can be made possible through use of the 'condition' elements, bypassing
1453objects that have already been processed (see how the
1454'core_create_placements' upgrade function declares conditions for an
1455example).
1456
1457=item * Beware which translate method to call
1458
1459$self->translate_escape is for messages and phrases which appear on the
1460progress screen (therefore they are sent in JSON).  Use MT->translate
1461to messages and phrases which directly stored in the database.  Log messages
1462and objects' attributes fall into this category.
1463
1464=back
1465
1466=head1 AUTHOR & COPYRIGHTS
1467
1468Please see the I<MT> manpage for author, copyright, and license information.
1469
1470=cut
Note: See TracBrowser for help on using the browser.