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

Revision 1927, 105.9 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::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 $App $DryRun $Installing $SuperUser
20            $CLI $MAX_TIME $MAX_ROWS @steps);
21sub BEGIN {
22    $MAX_TIME = 5;
23    $MAX_ROWS = 100;
24
25    %functions = (
26        # standard routines
27        'core_upgrade_begin' => {
28            code => \&core_upgrade_begin,
29            priority => 1,
30        },
31        'core_fix_type' => {
32            code => \&core_fix_type,
33            priority => 2,
34        },
35        'core_add_column' => {
36            code => sub { shift->core_column_action('add', @_) },
37            priority => 3,
38        },
39        'core_drop_column' => {
40            code => sub { shift->core_column_action('drop', @_) },
41            priority => 3,
42        },
43        'core_alter_column' => {
44            code => sub { shift->core_column_action('alter', @_) },
45            priority => 3,
46        },
47        'core_index_column' => {
48            code => sub { shift->core_column_action('index', @_) },
49            priority => 3.5,
50        },
51        'core_seed_database' => {
52            code => \&seed_database,
53            priority => 4,
54        },
55        'core_upgrade_templates' => {
56            code => \&upgrade_templates,
57            priority => 5,
58        },
59        'core_upgrade_end' => {
60            code => \&core_upgrade_end,
61            priority => 9,
62        },
63        'core_finish' => {
64            code => \&core_finish,
65            priority => 10,
66        },
67    );
68}
69
70sub core_upgrade_functions {
71    return {
72        # < 2.0
73        'core_create_placements' => {
74            version_limit => 2.0,
75            priority => 9.1,
76            updater => {
77                type => 'entry',
78                label => 'Creating entry category placements...',
79                condition => sub { $_[0]->category_id },
80                code => sub {
81                    require MT::Placement;
82                    my $entry = shift;
83                    my $existing = MT::Placement->load({ entry_id => $entry->id,
84                        category_id => $entry->category_id });
85                    if (!$existing) {
86                        my $place = MT::Placement->new;
87                        $place->entry_id($entry->id);
88                        $place->blog_id($entry->blog_id);
89                        $place->category_id($entry->category_id);
90                        $place->is_primary(1);
91                        $place->save;
92                    }
93                    $entry->category_id(0);
94                },
95            },
96        },
97        'core_create_template_maps' => {
98            version_limit => 2.0,
99            code => \&core_create_template_maps,
100            priority => 9.1,
101        },
102
103        # < 2.1
104        'core_fix_placement_blog_ids' => {
105            version_limit => 2.1,
106            priority => 9.2,
107            updater => {
108                type => 'placement',
109                label => 'Updating category placements...',
110                condition => sub { !$_[0]->blog_id },
111                code => sub {
112                    require MT::Category;
113                    my $cat = MT::Category->load($_[0]->category_id);
114                    $_[0]->blog_id($cat->blog_id) if $cat;
115                },
116            },
117        },
118
119        # < 3.0
120        'core_set_blog_allow_comments' => {
121            version_limit => 3.0,
122            priority => 9.3,
123            updater => {
124                type => 'blog',
125                label => 'Assigning comment/moderation settings...',
126                condition => sub { !(defined $_[0]->allow_unreg_comments ||
127                                     defined $_[0]->allow_reg_comments ||
128                                     defined $_[0]->manual_approve_comments ||
129                                     defined $_[0]->moderate_unreg_comments) },
130                code => sub {
131                    $_[0]->allow_unreg_comments(1)
132                        unless defined $_[0]->allow_unreg_comments;
133                    $_[0]->allow_reg_comments(1)
134                        unless defined $_[0]->allow_reg_comments;
135                    $_[0]->manual_approve_commenters(0)
136                        unless defined $_[0]->manual_approve_commenters;
137                    $_[0]->moderate_unreg_comments(0)
138                        unless defined $_[0]->moderate_unreg_comments;
139                    $_[0]->moderate_pings(0)
140                        unless defined $_[0]->moderate_pings;
141                },
142                sql => [
143                    'update mt_blog
144                        set blog_allow_unreg_comments = 1
145                      where blog_allow_unreg_comments is null',
146                    'update mt_blog
147                        set blog_allow_reg_comments = 1
148                      where blog_allow_reg_comments is null',
149                    'update mt_blog
150                        set blog_manual_approve_commenters = 0
151                      where blog_manual_approve_commenters is null',
152                    'update mt_blog
153                        set blog_moderate_unreg_comments = 0
154                      where blog_moderate_unreg_comments is null'
155                ],
156            },
157        },
158
159        # < 3.2
160        'core_set_default_basename_limit' => {
161            version_limit => 3.2,
162            priority => 9.3,
163            updater => {
164                type => 'blog',
165                label => 'Setting blog basename limits...',
166                condition => sub { !$_[0]->basename_limit },
167                code => sub { $_[0]->basename_limit(15) },
168                sql => 'update mt_blog set blog_basename_limit = 15
169                         where blog_basename_limit is null
170                            or blog_basename_limit < 15',
171            },
172        },
173        'core_set_default_blog_extension' => {
174            version_limit => 3.2,
175            priority => 9.3,
176            updater => {
177                type => 'blog',
178                label => 'Setting default blog file extension...',
179                condition => sub { !$_[0]->file_extension },
180                code => sub { $_[0]->file_extension('html') },
181            },
182        },
183        'core_set_enable_archive_paths' => {
184            version_limit => 3.2,
185            code => \&core_set_enable_archive_paths,
186            priority => 9.3,
187        },
188        # whereas init_comment_visible is done for adding new a comment visible
189        # to the comment table, this task sets all comments with a visible
190        # status of 2 to 0 since we now treat this field as a boolean.
191        'core_update_comment_visible' => {
192            version_limit => 3.2,
193            priority => 9.3,
194            updater => {
195                type => 'entry',
196                label => 'Updating comment status flags...',
197                condition => sub { $_[0]->allow_comments == 2 },
198                code => sub { $_[0]->allow_comments(0) },
199                sql => 'update mt_entry set entry_allow_comments = 0
200                         where entry_allow_comments = 2',
201            },
202        },
203        'core_remove_unique_constraints' => {
204            version_limit => 3.2,
205            code => \&core_remove_unique_constraints,
206            priority => 9.3,
207        },
208        'core_create_commenter_records' => {
209            version_limit => 3.2,
210            priority => 9.3,
211            updater => {
212                type => 'comment',
213                label => 'Updating commenter records...',
214                condition => sub { $_[0]->commenter_id },
215                code => sub {
216                    my ($comment) = @_;
217                    require MT::Permission; require MT::Author;
218                    my $perm = MT::Permission->load( { author_id => $comment->commenter_id,
219                        blog_id => $comment->blog_id } );
220                    if (!$perm) {
221                        if (my $cmtr = MT::Author->load($comment->commenter_id)) {
222                            $cmtr->pending($comment->blog_id);
223                        }
224                    }
225                }
226            }
227        },
228        'core_set_blog_admins' => {
229            version_limit => 3.2,
230            priority => 9.3,
231            updater => {
232                type => 'permission',
233                label => 'Assigning blog administration permissions...',
234                condition => sub { $_[0]->author_id && $_[0]->blog_id && $_[0]->can_edit_config },
235                code => sub {
236                    require MT::Association;
237                    require MT::Role;
238                    my ($role) = MT::Role->load_by_permission("administer_blog");
239                    if ($role) {
240                        my $user = MT::Author->load($_[0]->author_id);
241                        my $blog = MT::Author->load($_[0]->blog_id);
242                        if ($user && $blog) {
243                            MT::Association->link($role => $user => $blog);
244                        }
245                    } else {
246                        $_[0]->can_administer_blog(1)
247                    }
248                },
249            }
250        },
251        'core_set_blog_allow_pings' => {
252            version_limit => 3.2,
253            priority => 9.3,
254            updater => {
255                type => 'blog',
256                label => 'Setting blog allow pings status...',
257                condition => sub { !defined $_[0]->allow_pings },
258                code => sub { $_[0]->allow_pings($_[0]->allow_pings_default) },
259                sql => 'update mt_blog set blog_allow_pings = blog_allow_pings_default
260                         where blog_allow_pings is null',
261            }
262        },
263        'core_set_superuser' => {
264            version_limit => 3.2,
265            code => \&core_set_superuser,
266            priority => 9.3,
267        },
268        'core_conflate_require_email' => {
269            version_limit => 3.2,
270            priority => 9.3,
271            updater => {
272                type => 'blog',
273                label => 'Updating blog comment email requirements...',
274                condition => sub { !$_[0]->require_comment_emails },
275                code => sub { $_[0]->require_comment_emails(1)
276                                  if !$_[0]->allow_anon_comments },
277            }
278        },
279        'core_populate_entry_basenames' => {
280            version_limit => 3.2,
281            priority => 9.3,
282            updater => {
283                type => 'entry',
284                condition => sub { !$_[0]->basename },
285                code => sub { my $entry = shift; my %args = @_;
286                    $args{from} < 3.20021
287                        ? $entry->basename(mt32_dirify($entry->title))
288                        : 1;
289                },
290                label => 'Assigning entry basenames for old entries...',
291            }
292        },
293        'core_set_api_password' => {
294            version_limit => 3.2,
295            priority => 9.3,
296            updater => {
297                type => 'author',
298                condition => sub { ($_[0]->type == 1) && (($_[0]->api_password || '') eq '') },
299                code => sub { $_[0]->api_password($_[0]->password) },
300                label => 'Updating user web services passwords...',
301            }
302        },
303        'core_create_config_table' => {
304            version_limit => 3.2,
305            code => \&core_create_config_table,
306            priority => 9.3,
307        },
308        'core_deprecate_old_style_archive_links' => {
309            version_limit => 3.2,
310            priority => 9.3,
311            updater => {
312                type => 'blog',
313                label => 'Updating blog old archive link status...',
314                condition => sub { my $blog = shift; my %args = @_;
315                                   $blog->old_style_archive_links
316                                       || $args{from} < 3.0 },
317                code => sub {
318                    my ($blog) = @_;
319                    require MT::TemplateMap;
320                    foreach my $map (MT::TemplateMap->load({ blog_id => $blog->id })) {
321                        next if $map->file_template;
322           
323                        my $at = $map->archive_type;
324                        if ($at eq 'Individual') {
325                            $map->file_template('%e%x');
326                        } elsif ($at eq 'Daily') {
327                            $map->file_template('%y_%m_%d%x');
328                        } elsif ($at eq 'Weekly') {
329                            $map->file_template('week_%y_%m_%d%x');
330                        } elsif ($at eq 'Monthly') {
331                            $map->file_template('%y_%m%x');
332                        } elsif ($at eq 'Category') {
333                            $map->file_template('cat_%C%x');
334                        }
335                        $map->save;
336                    }
337                    $blog->old_style_archive_links(0);
338                }
339            }
340        },
341        'core_set_entry_weeknumber' => {
342            version_limit => 3.20006,
343            priority => 9.3,
344            updater => {
345                type => 'entry',
346                condition => sub { ($_[0]->week_number || 0) < 54 },
347                code => sub { 1 },
348                label => 'Updating entry week numbers...',
349            }
350        },
351        'core_set_tag_permissions' => {
352            version_limit => 3.20021,
353            priority => 9.3,
354            updater => {
355                type => 'permission',
356                condition => sub { !$_[0]->can_edit_tags && !$_[0]->can_administer_blog },
357                code => sub { $_[0]->can_edit_tags($_[0]->can_edit_categories) },
358                label => 'Updating user permissions for editing tags...',
359            }
360        },
361        'core_init_blog_entry_display_defaults' => {
362            version_limit => 3.20021,
363            code => sub {
364                require MT::Permission;
365                &core_update_records
366            },
367            priority => 9.3,
368            updater => {
369                type => 'blog',
370                condition => sub { !(MT::Permission->exist({
371                    blog_id => $_[0]->id, author_id => 0 })) },
372                code => sub {
373                    my $perm = new MT::Permission;
374                    $perm->entry_prefs('Advanced|Bottom');
375                    $perm->blog_id($_[0]->id);
376                    $perm->author_id(0);
377                    $perm->save;
378                },
379                label => 'Setting new entry defaults for blogs...',
380            }
381        },
382        'core_upgrade_tag_categories' => {
383            version_limit => 3.20021,
384            priority => 9.3,
385            updater => {
386                type => 'category',
387                condition => sub { ($_[0]->description||'') =~ m/<!--tag-->/ },
388                code => sub {
389                    my ($cat) = @_;
390                    require MT::Placement;
391                    require MT::Entry;
392                    my @e = MT::Entry->load(undef, {join => ['MT::Placement', 'entry_id', { category_id => $cat->id }]});
393                    my $tag_name = $cat->label;
394                    foreach my $e (@e) {
395                        my $tags = $e->tags;
396                        $e->tags($tags, $tag_name);
397                        $e->save;
398                    }
399                },
400                label => 'Migrating any "tag" categories to new tags...',
401            }
402        },
403        'core_init_blog_custom_dynamic_templates' => {
404            on_field => 'MT::Blog->custom_dynamic_templates',
405            priority => 3.1,
406            updater => {
407                type => 'blog',
408                condition => sub { !defined $_[0]->custom_dynamic_templates },
409                code => sub { $_[0]->custom_dynamic_templates('none') },
410                label => 'Assigning custom dynamic template settings...',
411                sql => q{update mt_blog
412                            set blog_custom_dynamic_templates = 'none'
413                          where blog_custom_dynamic_templates is null},
414            }
415        },
416        'core_init_author_type' => {
417            on_field => 'MT::Author->type',
418            priority => 3.1,
419            updater => {
420                type => 'author',
421                condition => sub { !$_[0]->type },
422                code => sub { $_[0]->type(1) },
423                label => 'Assigning user types...',
424                sql => 'update mt_author set author_type = 1
425                        where author_type is null or author_type = 0',
426            }
427        },
428        'core_init_category_parent' => {
429            on_field => 'MT::Category->parent',
430            priority => 3.1,
431            updater => {
432                type => 'category',
433                condition => sub { !defined $_[0]->parent },
434                code => sub { $_[0]->parent(0) },
435                label => 'Assigning category parent fields...',
436                sql => 'update mt_category set category_parent = 0
437                        where category_parent is null',
438            }
439        },
440        'core_init_template_build_dynamic' => {
441            on_field => 'MT::Template->build_dynamic',
442            priority => 3.1,
443            updater => {
444                type => 'template',
445                condition => sub { !defined $_[0]->build_dynamic },
446                code => sub { $_[0]->build_dynamic(0) },
447                label => 'Assigning template build dynamic settings...',
448                sql => 'update mt_template set template_build_dynamic = 0
449                        where template_build_dynamic is null',
450            }
451        },
452        'core_init_comment_visible' => {
453            on_field => 'MT::Comment->visible',
454            priority => 3.1,
455            updater => {
456                type => 'comment',
457                condition => sub { !defined $_[0]->visible },
458                code => sub { $_[0]->visible(1) },
459                label => 'Assigning visible status for comments...',
460                sql => 'update mt_comment set comment_visible = 1
461                        where comment_visible is null',
462            }
463        },
464        'core_init_comment_junk_status' => {
465            version_limit => 4.0053,
466            priority => 3.1,
467            updater => {
468                type => 'comment',
469                condition => sub { !$_[0]->junk_status },
470                code => sub { $_[0]->junk_status(1) },
471                label => 'Assigning junk status for comments...',
472                sql => 'update mt_comment set comment_junk_status = 1
473                        where comment_junk_status is null
474                           or comment_junk_status=0',
475            }
476        },
477        'core_init_tbping_visible' => {
478            on_field => 'MT::TBPing->visible',
479            priority => 3.1,
480            updater => {
481                type => 'tbping',
482                condition => sub { !defined $_[0]->visible },
483                code => sub { $_[0]->visible(1) },
484                label => 'Assigning visible status for TrackBacks...',
485                sql => 'update mt_tbping set tbping_visible = 1
486                        where tbping_visible is null',
487            }
488        },
489        'core_init_tbping_junk_status' => {
490            version_limit => 4.0053,
491            priority => 3.1,
492            updater => {
493                type => 'tbping',
494                condition => sub { !$_[0]->junk_status },
495                code => sub { $_[0]->junk_status(1) },
496                label => 'Assigning junk status for TrackBacks...',
497                sql => 'update mt_tbping set tbping_junk_status = 1
498                        where tbping_junk_status is null
499                          or tbping_junk_status=0',
500            }
501        },
502        'core_init_category_basename' => {
503            version_limit => 3.2002,
504            priority => 3.1,
505            updater => {
506                type => 'category',
507                condition => sub { !defined $_[0]->basename },
508                code => sub { my $cat = shift; my %args = @_;
509                    $args{from} < 3.20021
510                        ? $cat->basename(mt32_dirify($cat->label))
511                        : 1;
512                },
513                label => 'Assigning basename for categories...',
514            }
515        },
516        'core_set_author_status' => {
517            version_limit => 3.301,
518            priority => 3.1,
519            updater => {
520                type => 'author',
521                label => 'Assigning user status...',
522                condition => sub {
523                    ($_[0]->type == 1) && (!defined $_[0]->status)
524                },
525                code => sub {
526                    shift->status(1);
527                },
528                sql => 'update mt_author set author_status = 1
529                        where author_type = 1 and author_status is null',
530            }
531        },
532        'core_install_default_roles' => {
533            code => \&create_default_roles,
534            on_class => 'MT::Role',
535            priority => 3.1,
536        },
537        'core_migrate_permissions_to_roles' => {
538            version_limit => 3.3101,
539            priority => 3.2,
540            updater => {
541                type => 'permission',
542                label => 'Migrating permissions to roles...',
543                condition => sub { $_[0]->blog_id },
544                code => \&_migrate_permission_to_role,
545            }
546        },
547        'core_remove_dynamic_site_bootstrapper' => {
548            code => \&remove_mtviewphp,
549            version_limit => 3.3101,
550            priority => 5.1,
551        },
552        'core_migrate_commenter_auth' => {
553            code => \&migrate_commenter_auth,
554            version_limit => 3.3101,
555            priority => 3.1,
556        },
557        'core_deprecate_bitmask_permissions' => {
558            code => \&deprecate_bitmask_permissions,
559            version_limit => 4.0002,
560            priority => 3.3,
561        },
562        'core_migrate_system_privileges' => {
563            code => \&migrate_system_privileges,
564            version_limit => 4.0002,
565            priority => 3.3,
566        },
567        'core_populate_authored_on' => {
568            version_limit => 4.0014,
569            priority => 3.1,
570            updater => {
571                type => 'entry',
572                label => 'Populating authored and published dates for entries...',
573                condition => sub {
574                    !defined $_[0]->authored_on
575                },
576                code => sub {
577                    $_[0]->authored_on($_[0]->created_on)
578                        if !defined $_[0]->authored_on;
579                },
580                sql =>
581                    'update mt_entry set entry_authored_on = entry_created_on
582                        where entry_authored_on is null',
583            },
584        },
585        'core_update_3x_system_search_templates' => {
586            version_limit => 4.0017,
587            priority => 3.1,
588            code => \&update_3x_system_search_templates,
589        },
590        'core_rename_php_plugin_filenames' => {
591            version_limit => 4.0019,
592            priority => 3.1,
593            code => \&rename_php_plugin_filenames,
594        },
595        'core_migrate_nofollow_settings' => {
596            version_limit => 4.0020,
597            priority => 3.1,
598            code => \&migrate_nofollow_settings,
599        },
600        'core_update_widget_template' => {
601            version_limit => 4.0022,
602            priority => 3.1,
603            code => \&update_widget_templates,
604        },
605        # This upgrade step is currently necessary for PostgreSQL
606        # which doesn't support adding a column, populating the existing
607        # records with a value.
608        'core_typify_category_records' => {
609            version_limit => 4.0023,
610            priority => 3.1,
611            updater => {
612                type => 'category',
613                label => 'Classifying category records...',
614                condition => sub {
615                    !$_[0]->column('class')
616                },
617                code => sub {
618                    $_[0]->class('category');
619                },
620                sql =>
621                    "update mt_category set category_class = 'category'
622                        where category_class is null",
623            },
624        },
625        'core_typify_entry_records' => {
626            version_limit => 4.0023,
627            priority => 3.1,
628            updater => {
629                type => 'entry',
630                label => 'Classifying entry records...',
631                condition => sub {
632                    !$_[0]->column('class')
633                },
634                code => sub {
635                    $_[0]->class('entry');
636                },
637                sql =>
638                    "update mt_entry set entry_class = 'entry'
639                        where entry_class is null",
640            },
641        },
642        'core_merge_comment_response_templates' => {
643            version_limit => 4.0023,
644            priority => 3.1,
645            updater => {
646                type => 'blog',
647                label => "Merging comment system templates...",
648                code => \&_merge_comment_response_templates_updater,
649            },
650        },
651        'core_populate_default_file_template' => {
652            version_limit => 4.0023,
653            priority => 3.1,
654            updater => {
655                type => 'templatemap',
656                label => 'Populating default file template for templatemaps...',
657                condition => sub {
658                    !defined $_[0]->file_template
659                },
660                code => sub {
661                    my %default_template = (
662                        Individual => '%y/%m/%f',
663                        Category => '%c/%i',
664                    );
665                    $_[0]->file_template($default_template{$_[0]->archive_type})
666                        if !defined $_[0]->file_template && exists($default_template{$_[0]->archive_type});
667                },
668            },
669        },
670        'core_remove_unused_templatemap' => {
671            version_limit => 4.0023,
672            priority => 3.0,
673            updater => {
674                type => 'blog',
675                label => 'Removing unused template maps...',
676                condition => sub {
677                    my $blog = shift;
678                    my @blog_at = map { "'$_'" } split ',', $blog->archive_type;
679                    MT::TemplateMap->remove(
680                      { blog_id => $blog->id, archive_type => \@blog_at },
681                      { not => { archive_type => 1 } }
682                    ) if @blog_at;
683                    return 0;
684                },
685            },
686        },
687        'core_set_author_auth_type' => {
688            version_limit => 4.0024,
689            priority => 3.2,
690            updater => {
691                type => 'author',
692                label => 'Assigning user authentication type...',
693                condition => sub {
694                    !$_[0]->auth_type;
695                },
696                code => \&core_populate_author_auth_type,
697            },
698        },
699        'core_add_newfeature_widget' => {
700            version_limit => 4.0027,
701            priority => 3.2,
702            updater => {
703                type => 'author',
704                label => 'Adding new feature widget to dashboard...',
705                condition => sub {
706                    my ($user) = @_;
707                    $user->type == MT::Author::AUTHOR(); # AUTHOR records only
708                },
709                code => sub {
710                    my ($user) = @_;
711                    my $widget_store = $user->widgets();
712                    if ($widget_store && %$widget_store) {
713                        for my $set (keys %$widget_store) {
714                            $widget_store->{$set}->{new_version} = {
715                                template => 'widget/new_version.tmpl',
716                                set      => 'main',
717                                singular => 1,
718                                order    => -1,
719                            };
720                        }
721                    }
722                    $user->widgets($widget_store);
723                },
724            },
725        },
726        'core_add_email_template' => {
727            version_limit => 4.0030,
728            priority => 9.3,
729            code => sub {
730                my $self = shift;
731                $self->upgrade_templates({ Install => 1 });
732            },
733        },
734        'core_replace_openid_username' => {
735            version_limit => 4.0033,
736            priority => 3.2,
737            updater => {
738                type => 'author',
739                label => 'Moving OpenID usernames to external_id fields...',
740                condition => sub {
741                    return 0 if 'MT' eq $_[0]->auth_type;
742                    my $auth = MT->commenter_authenticator($_[0]->auth_type);
743                    return 0 unless $auth && %$auth && exists($auth->{class});
744                    my $auth_class = $auth->{class};
745                    eval "require $auth_class;";
746                    return 0 if $@;
747                    return UNIVERSAL::isa($auth_class, 'MT::Auth::OpenID');
748                },
749                code => sub {
750                    return unless $_[0]->url;
751                    my $existing = MT::Author->load({ name => $_[0]->url, auth_type => 'OpenID' });
752                    unless ($existing) {
753                        $_[0]->external_id($_[0]->name);
754                        $_[0]->name($_[0]->url);
755                    }
756                },
757            },
758        },
759        'core_set_template_set' => {
760            version_limit => 4.0034,
761            priority => 3.2,
762            updater => {
763                type => 'blog',
764                label => 'Assigning blog template set...',
765                condition => sub {
766                    !$_[0]->template_set;
767                },
768                code => sub {
769                    $_[0]->template_set('mt_blog');
770                    MT->run_callbacks( 'blog_template_set_change', { blog => $_[0] } );
771                },
772            },
773        },
774        'core_set_page_layout' => {
775            version_limit => 4.0036,
776            priority => 3.2,
777            updater => {
778                type => 'blog',
779                label => 'Assigning blog page layout...',
780                condition => sub {
781                    !$_[0]->page_layout;
782                },
783                code => sub {
784                    my ($blog) = @_;
785                    my $layout = 'layout-wtt';
786                    require MT::Template;
787                    my $styles = MT::Template->load({ blog_id => $blog->id, identifier => 'styles' });
788                    if ($styles) {
789                        if ($styles->text =~ m{ <mt:?setvar \s+ name="page_layout" \s+ value="([^"]+)"> }xmsi) {
790                            $layout = $1;
791                        }
792                    }
793                    $blog->page_layout($layout);
794                },
795            },
796        },
797        'core_set_author_basename' => {
798            version_limit => 4.0037,
799            priority => 3.2,
800            updater => {
801                type => 'author',
802                label => 'Assigning author basename...',
803                condition => sub {
804                    $_[0]->type == 1;
805                },
806                code => sub {
807                    my ($author) = @_;
808                    my $basename = MT::Util::make_unique_author_basename($author);
809                    $author->basename($basename);
810                },
811            },
812        },
813        'core_remove_indexes' => {
814            version_limit => 4.0041,
815            priority => 3.2,
816            code => \&remove_indexes,
817        },
818        'core_set_count_columns' => {
819            version_limit => 4.0047,
820            priority      => 3.2,
821            updater       => {
822                type      => 'entry',
823                label     => 'Assigning entry comment and TrackBack counts...',
824                condition => sub {
825                    require MT::Comment;
826                    my $comment_count = MT::Comment->count(
827                        {
828                            entry_id => $_[0]->id,
829                            visible  => 1,
830                        }
831                    );
832                    $_[0]->comment_count($comment_count);
833                    require MT::Trackback;
834                    require MT::TBPing;
835                    my $tb = MT::Trackback->load( { entry_id => $_[0]->id } );
836                    my $ping_count;
837                    if ($tb) {
838                        my $ping_count = MT::TBPing->count(
839                            {
840                                tb_id   => $tb->id,
841                                visible => 1,
842                            }
843                        );
844                        $_[0]->ping_count($ping_count);
845                    }
846                    ( $comment_count || $ping_count );
847                },
848                # only count once and set it, so code do nothing.
849                # it doesn't have the unnecessary save.
850                code => sub { 1; },
851            },
852        },
853        'core_assign_object_embedded' => {
854            version_limit => 4.0052,
855            priority => 3.2,
856            updater => {
857                type => 'objectasset',
858                label => 'Assigning embedded flag to asset placements...',
859                code => sub {
860                    $_[0]->embedded(1);
861                },
862                sql => 'update mt_objectasset set objectasset_embedded=1',
863            },
864        },
865        'core_enable_address_book' => {
866            version_limit => 4.0054,
867            priority => 3.2,
868            code => sub {
869                require MT::Notification;
870                if (MT::Notification->exist()) {
871                    my $cfg = MT->config;
872                    if (! $cfg->EnableAddressBook) {
873                        $cfg->EnableAddressBook(1, 1);
874                        $cfg->save;
875                    }
876                }
877                return 0;
878            },
879        },
880    }
881}
882
883sub core_populate_author_auth_type {
884    my ($u) = @_;
885    if ($u->type == 1) {
886        $u->auth_type(MT->config->AuthenticationModule || 'MT');
887    } else {
888        # for legacy OpenID plugin commenters
889        if ($u->name =~ m(^openid\n(.*)$)) {
890            my $url = $1;
891            if (eval { require Digest::MD5; 1; }) {
892                $url = Digest::MD5::md5_hex($url);
893            } else {
894                $url = substr $url, 0, 255;
895            }
896            $u->name($url);
897            $u->auth_type('OpenID');
898        }
899        elsif ($u->name =~ m!^[a-f0-9]{32}$!) {
900            # Vox OpenID URL; set auth_type to 'Vox'
901            if ($u->url =~ m!\.vox\.com/!) {
902                $u->auth_type('Vox');
903            }
904            # LJ OpenID URL; set auth_type to 'LiveJournal'
905            elsif ($u->url =~ m!\.livejournal\.com/!) {
906                $u->auth_type('LiveJournal');
907            }
908            else {
909                # Other custom auth, which for now means OpenID
910                $u->auth_type('OpenID');
911            }
912        }
913        else {
914            # Default to TypeKey for remaining plain name fields
915            $u->auth_type('TypeKey');
916        }
917    }
918}
919
920sub migrate_nofollow_settings {
921    my $self = shift;
922
923    $self->progress($self->translate_escape("Migrating Nofollow plugin settings..."));
924    require MT::PluginData;
925    my $cfg = MT->config;
926    my $plugins = $cfg->PluginSwitch || {};
927    my $nofollow_switch = $plugins->{'nofollow/nofollow.pl'};
928    my $enabled = defined $nofollow_switch ? ($nofollow_switch ? 1 : 0) : 1;
929    my $default_follow_auth_links = 1;
930
931    # For any configuration settings that exist
932    my @config = MT::PluginData->load({ plugin => 'Nofollow' });
933    my %blogs_saved;
934    foreach my $cfg (@config) {
935        if ($cfg->key =~ m/^configuration:blog:(\d+)/) {
936            my $blog = MT::Blog->load($1) or next;
937            my $setting = ($cfg->data || {})->{follow_auth_links};
938            $blog->follow_auth_links($setting) if defined $setting;
939            $blog->nofollow_urls($enabled);
940            $blog->save;
941            $blogs_saved{$blog->id} = 1;
942        } else {
943            my $setting = ($cfg->data || {})->{follow_auth_links};
944            $default_follow_auth_links = $setting if defined $setting;
945        }
946    }
947    $_->remove for @config;
948
949    my $blog_iter = MT::Blog->load_iter;
950    while (my $blog = $blog_iter->()) {
951        next if exists $blogs_saved{$blog->id};
952        $blog->nofollow_urls($enabled);
953        $blog->follow_auth_links($default_follow_auth_links);
954        $blog->save;
955    }
956
957    # Forcibly disable nofollow plugin now since this has become
958    # a core function.
959    $cfg->PluginSwitch('nofollow/nofollow.pl=0', 1);
960    $cfg->save_config();
961
962    return 0;
963}
964
965sub update_3x_system_search_templates {
966    my $self = shift;
967
968    require MT::Template;
969    $self->progress($self->translate_escape('Updating system search template records...'));
970    my $tmpl_iter = MT::Template->load_iter({
971        type => 'search_template',
972    });
973    my %blogs;
974    while (my $tmpl = $tmpl_iter->()) {
975        $blogs{$tmpl->blog_id} = $tmpl->id;
976        $tmpl->type('search_results');
977        $tmpl->save;
978    }
979    # for any old 'search_template' system templates, remove the
980    # newly installed 'search_results' template.
981    foreach my $blog_id (keys %blogs) {
982        my $tmpl = MT::Template->load({ type => 'search_results',
983            blog_id => $blog_id, id => $blogs{$blog_id} }, {
984            not => { id => 1 } });
985        $tmpl->remove if $tmpl;
986    }
987    if (my @blog_ids = keys %blogs) {
988        MT::Template->remove(
989            { type => 'search_results', blog_id => \@blog_ids },
990            { not => { blog_id => 1 } }
991        );
992    }
993    0;
994}
995
996my $perm_role_names = {
997    4096 => 'Blog Administrator',  # administer_blog
998    30687 => 'Blog Administrator', # 32767 - 2048(not comment) - 32(reserved) = all permissions in MT3.3
999    14303 => 'Blog Administrator', # 16383 - 2048(not comment) - 32(reserved) = all permissions in MT3.2
1000    2 => 'Writer', # post
1001    6 => 'Writer (can upload)', # post + upload
1002    17032 => 'Editor',  # edit_all_posts + edit_tags + edit_categories + rebuild
1003    17036 => 'Editor (can upload)', # Editor + upload
1004    144 => 'Designer', # edit_templates + rebuild
1005    17292 => 'Publisher', # Editor (can upload) + send_notifications
1006};
1007
1008{
1009    my $full_perm_mask = 0;
1010    my %LegacyPerms = (
1011        # System-wide permissions
1012        #[ 2**0, 'administer', 'System Administrator', 2, 'system' ],
1013        #[ 2**1, 'create_blog', 'Create Blogs', 2, 'system' ],
1014        #[ 2**2, 'view_log', 'View System Activity Log', 2, 'system' ],
1015        #[ 2**3, 'manage_plugins', 'Manage Plugins', 'system' ],
1016 
1017        # Blog-specific permissions:
1018        # The order here is the same order they are presented on the
1019        # role definition screen.
1020        2**0 => 'comment',# 'Add Comments', 1, 'blog'],
1021        2**12 => 'administer_blog',# 'Blog Administrator', 1, 'blog'],
1022        2**6 => 'edit_config',# 'Configure Blog', 1, 'blog'],
1023        2**3 => 'edit_all_posts',# 'Edit All Entries', 1, 'blog'],
1024        2**4 => 'edit_templates',# 'Manage Templates', 1, 'blog'],
1025        2**2 => 'upload',# 'Upload File', 1, 'blog'],
1026        2**1 => 'post',# 'Create Entry', 1, 'blog'],
1027        2**16 => 'edit_assets',# 'Manage Assets', 1, 'blog'],
1028        2**15 => 'save_image_defaults',# 'Save Image Defaults', 1, 'blog'],
1029        2**9 => 'edit_categories',# 'Add/Manage Categories', 1, 'blog'],
1030        2**14 => 'edit_tags',# 'Manage Tags', 1, 'blog'],
1031        2**10 => 'edit_notifications',# 'Manage Notification List', 1, 'blog'],
1032        2**8 => 'send_notifications',# 'Send Notifications', 1, 'blog'],
1033        2**13 => 'view_blog_log',# 'View Activity Log', 1, 'blog'],
1034        #[ 2**17, 'publish_post', 'Publish Post', 1, 'blog'],
1035        #[ 2**18, 'manage_feedback', 'Manage Feedback', 1, 'blog'],
1036        #[ 2**19, 'set_publish_paths', 'Set Publishing Paths', 1, 'blog'],
1037        #[ 2**20, 'manage_pages', 'Manage Pages', 1, 'blog'],
1038        # 2**5 == 32 is deprecated; reserved for future use
1039        2**7 => 'rebuild',# 'Rebuild Files', 1, 'blog'],
1040        # Not a real permission but a denial thereeof; unlisted because it
1041        # has no label.
1042        2**11 => 'not_comment',# '', 1, 'blog'],
1043    );
1044
1045
1046sub _migrate_permission_to_role {
1047    my $perm = shift;
1048
1049    return unless $perm->author_id;
1050    my $user = MT::Author->load($perm->author_id);
1051    if (!$user) {
1052        $perm->remove;
1053        return;
1054    }
1055    # Don't bother with non-AUTHOR types
1056    return unless $user->type == 1;
1057
1058    my $role_mask = $perm->role_mask;
1059    $role_mask -= 32 if (32 & $role_mask) == 32; # for permissions before 3.2
1060
1061    if (!$full_perm_mask) {
1062        # only consider blog permissions that are supported (exclude
1063        # now reserved permission bits like 32).
1064        foreach my $key (keys %LegacyPerms) {
1065            next if $LegacyPerms{$key} =~ m/^not_/; # skip exclusion permissions
1066            $full_perm_mask |= $key;
1067        }
1068    }
1069
1070    $role_mask = $full_perm_mask & $role_mask;
1071
1072    # '0' permission, not used for permissions, just prefs
1073    return unless $role_mask;
1074
1075    my $name;
1076    $name = MT->translate($perm_role_names->{$role_mask})
1077        if $perm_role_names->{$role_mask};
1078    $name ||= MT->translate("Custom ([_1])", $role_mask);
1079    require MT::Role;
1080    my $role = MT::Role->load({ name => $name });
1081    if ($role) {
1082        if (($role->role_mask != $role_mask) &&
1083            ((4096 != $role->role_mask) && (30687 != $role_mask))) {
1084            $role = undef;
1085        }
1086    }
1087    unless ($role) {
1088        $role = new MT::Role;
1089        $role->name($name);
1090        $role->description(MT->translate("This role was generated by Movable Type upon upgrade."));
1091        $role->role_mask($role_mask);
1092        $role->save;
1093    }
1094    my $blog = MT::Blog->load($perm->blog_id);
1095    $user->add_role($role, $blog) if $blog;
1096}
1097
1098sub _process_masks {
1099    my $self = shift;
1100    my ($perm) = @_;
1101
1102    my $mask = $perm->role_mask;
1103    return unless $mask;
1104    my @perms;
1105    for my $key (keys %LegacyPerms) {
1106        if (int($mask) & int($key)) {
1107            if (2 eq $key) { # post
1108                push @perms, 'create_post', 'publish_post';
1109            } elsif (64 eq $key) { #edit_config
1110                push @perms, 'edit_config', 'set_publish_paths', 'manage_feedback';
1111            } elsif (4096 eq $key) { #adminsiter_blog
1112                push @perms, 'administer_blog', 'manage_pages';
1113            } elsif (2048 eq $key) { #not_comment
1114                $perm->restrictions("'comment'");
1115            } else {
1116                push @perms, $LegacyPerms{$key};
1117            }
1118        }
1119    }
1120    my $perm_str = scalar(@perms) ? "'" . join("','", @perms) . "'" : q();
1121    $perm->permissions($perm_str);
1122    $perm->role_mask(0); ## remove legacy permissions
1123    $perm;
1124}
1125
1126sub deprecate_bitmask_permissions {
1127    my $self = shift;
1128   
1129    require MT::Permission;
1130    my $perm_iter = MT::Permission->load_iter;
1131    $self->progress($self->translate_escape('Migrating permission records to new structure...'));
1132    while (my $perm = $perm_iter->()) {
1133        if ($self->_process_masks($perm)) {
1134            $perm->save;
1135        }
1136    }
1137
1138    require MT::Role;
1139    my $role_iter = MT::Role->load_iter;
1140    $self->progress($self->translate_escape('Migrating role records to new structure...'));
1141    while (my $role = $role_iter->()) {
1142        if ($self->_process_masks($role)) {
1143            # do not have to rebuild permissions here.
1144            # "save" here causes segfault in sqlite.
1145            $role->update;
1146        }
1147    }
1148}
1149}
1150
1151sub migrate_system_privileges {
1152    my $self = shift;
1153
1154    require MT::Permission;
1155    my $author_iter = MT::Author->load_iter({ type => MT::Author::AUTHOR() });
1156    $self->progress($self->translate_escape('Migrating system level permissions to new structure...'));
1157    while (my $author = $author_iter->()) {
1158        my @perms;
1159        push @perms, 'administer' if $author->column('is_superuser');
1160        push @perms, 'create_blog' if $author->column('can_create_blog') || $author->column('is_superuser');
1161        push @perms, 'view_log' if $author->column('can_view_log') || $author->column('is_superuser');
1162        push @perms, 'manage_plugins' if $author->column('is_superuser');
1163        if (@perms) {
1164            my $perm = MT::Permission->load({ author_id => $author->id,
1165                blog_id => 0 });
1166            if (!$perm) {
1167                $perm = MT::Permission->new;
1168                $perm->author_id($author->id);
1169                $perm->blog_id(0);
1170            }
1171            $perm->set_these_permissions(@perms);
1172            $perm->save;
1173        }
1174    }
1175}
1176
1177sub init {
1178    my $pkg = shift;
1179    unless (%classes) {
1180        my $types = MT->registry('object_types');
1181        foreach my $type (keys %$types) {
1182            $classes{$type} = $types->{$type};
1183        }
1184    }
1185    my $fns = MT::Component->registry('upgrade_functions') || [];
1186    foreach my $fn_set (@$fns) {
1187        %functions = ( %functions, %{ $fn_set } );
1188    }
1189}
1190
1191# Step execution...
1192
1193# iterate routines:
1194#     no parameters, start with offset == 0
1195#     offset parameter, pass thru
1196#     if routine returns 0, routine is done
1197#     if routine returns undef, routine failed
1198#     if routine returns > 0, that's the new offset
1199
1200sub run_step {
1201    my $self = shift;
1202    my ($step) = @_;
1203    my ($name, %param) = @$step;
1204
1205    if (my $fn = $functions{$name}) {
1206        local $MT::CallbacksEnabled = 0;
1207        if (my $cond = $fn->{condition}) {
1208            $cond = MT->handler_to_coderef($cond);
1209            next unless $cond->($self, %param);
1210        }
1211        my %update_params;
1212        if ($fn->{updater}) {
1213            %update_params = %{$fn->{updater}};
1214            $fn->{code} ||= \&core_update_records;
1215        }
1216        my $code = $fn->{code} || $fn->{handler};
1217        $code = MT->handler_to_coderef($code);
1218        my $result = $code->($self, %param, %update_params, step => $name);
1219        if ((defined $result) && ($result > 1)) {
1220            $param{offset} = $result; $result = 1;
1221            $self->add_step($name, %param);
1222        }
1223        return $result;
1224    } else {
1225        return $self->error($self->translate_escape("Invalid upgrade function: [_1].", $name));
1226    }
1227    0;
1228}
1229
1230sub run_callbacks {
1231    my $self = shift;
1232    my ($cb, @param) = @_;
1233    local $MT::CallbacksEnabled = 1;
1234    MT->run_callbacks('MT::Upgrade::' . $cb, $self, @param);
1235}
1236
1237# Main "do" interface for controlling apparatus
1238
1239sub do_upgrade {
1240    my $self = shift;
1241    my (%opt) = @_;
1242
1243    $self->init;
1244
1245    my $harnessed = ref $opt{App} && (UNIVERSAL::can($opt{App}, 'add_step'));
1246
1247    local $App = $opt{App};
1248    local $DryRun = $opt{DryRun};
1249    local $SuperUser = $opt{SuperUser} || '';
1250    local $CLI = $opt{CLI} || '';
1251
1252    @steps = ();
1253    if ($opt{Install}) {
1254        my %init_params = (%{$opt{User} || {}}, %{$opt{Blog} || {}});
1255        $self->install_database(\%init_params);
1256    } else {
1257        $self->upgrade_database();
1258    }
1259
1260    # no app is running the show, so we must!
1261    if (!$harnessed) {
1262        # set these limits very high since we're running unharnessed
1263        $MAX_TIME = 10000000;
1264        $MAX_ROWS = 300;
1265        my $fn = \%MT::Upgrade::functions;
1266        my @these_steps = @steps;
1267        while (@these_steps) {
1268            my $step = shift @these_steps;
1269            @steps = ();
1270            $self->run_step($step);
1271            if (@steps) {
1272                push @these_steps, @steps;
1273                @these_steps = sort { $fn->{$a->[0]}->{priority} <=>
1274                                      $fn->{$b->[0]}->{priority} } @these_steps;
1275            }
1276        }
1277        return 1;
1278    } else {
1279        return \@steps;
1280    }
1281}
1282
1283sub upgrade_database {
1284    my $self = shift;
1285
1286    my $config_schema_ver;
1287    my $schema_ver;
1288    if ($config_schema_ver = MT->instance->config('SchemaVersion')) {
1289        my $needs_upgrade;
1290        $needs_upgrade = 1 if $config_schema_ver < MT->schema_version;
1291        if (!$needs_upgrade) {
1292            foreach (@MT::Components) {
1293                $needs_upgrade = 1 if $_->needs_upgrade;
1294            }
1295        }
1296        return 1 unless $needs_upgrade;
1297        $schema_ver = $config_schema_ver;
1298    } else {
1299        $schema_ver = $self->detect_schema_version;
1300    }
1301
1302    # this will add steps to upgrade all tables that need it...
1303    $self->add_step("core_upgrade_begin", from => $schema_ver);
1304    $self->check_schema;
1305    $self->add_step('core_upgrade_templates');
1306    $self->add_step('core_upgrade_end', from => $schema_ver);
1307    $self->add_step('core_finish');
1308    1;
1309}
1310
1311sub install_database {
1312    my $self = shift;
1313    my ($user) = @_;
1314
1315    # this will add steps to install all tables...
1316    $self->check_schema;
1317    # this will populate them...
1318    $self->add_step('core_seed_database', %$user);
1319    $self->add_step('core_upgrade_templates');
1320    $self->add_step('core_finish');
1321    1;
1322}
1323
1324sub check_schema {
1325    my $self = shift;
1326    my $class;
1327    foreach my $type (keys %classes) {
1328        $class = MT->model($type)
1329            or return $self->error($self->translate_escape("Error loading class [_1].", $type));
1330        $self->check_type($type);
1331    }
1332    1;
1333}
1334
1335sub check_type {
1336    my $self = shift;
1337    my ($type) = @_;
1338
1339    my $class = MT->model($type);
1340    if (my $result = $self->type_diff($type)) {
1341        if ($result->{fix}) {
1342            $self->add_step('core_fix_type', type => $type);
1343        } else {
1344            $self->add_step('core_add_column', type => $type)
1345                if $result->{add};
1346            $self->add_step('core_alter_column', type => $type)
1347                if $result->{alter};
1348            $self->add_step('core_drop_column', type => $type)
1349                if $result->{drop};
1350            $self->add_step('core_index_column', type => $type)
1351                if $result->{index};
1352        }
1353    }
1354
1355    # handle schema updates for meta table
1356    if ($class->meta_pkg) {
1357        $self->check_type($type . ':meta');
1358    }
1359
1360    1;
1361}
1362
1363sub type_diff {
1364    my $self = shift;
1365    my ($type) = @_;
1366
1367    my $class = MT->model($type) or return;
1368
1369    my $table = $class->datasource;
1370    my $defs = $class->column_defs;
1371
1372    my $ddl = $class->driver->dbd->ddl_class;
1373    my $db_defs = $ddl->column_defs($class);
1374
1375    my $class_idx_defs = $class->index_defs;
1376    my $db_idx_defs = $ddl->index_defs($class);
1377
1378    # now, compare $defs and $db_defs;
1379    # here are the scenarios
1380    #   1. we find something in $defs that isn't in $db_defs
1381    #      -- column should be inserted. this may trigger a process
1382    #   2. we find something in $db_defs that isn't in $defs
1383    #      -- this is a-ok. user may have added a column.
1384    #   3. we find a difference between $defs and $db_defs for a field
1385    #      a. type differs; this may trigger a process
1386    #      b. type is same, but null property differs; this may
1387    #         trigger a process
1388    #      c. type is same, but size differs; this may trigger a process
1389    #      d. key differs
1390    #      e. auto differs (auto-increment)
1391    #   4. table doesn't exist and must be created
1392
1393    my $fix_class;
1394    $fix_class = 1 unless defined $db_defs;
1395
1396    # we're only scanning defined columns; we don't care about
1397    # columns that are unique to the table.
1398    my (@cols_to_add, @cols_to_alter, @cols_to_drop, @cols_to_index);
1399
1400    if (!$fix_class) {
1401        my @def_cols = keys %$defs;
1402
1403        foreach my $col (@def_cols) {
1404            my $col_def = $defs->{$col};
1405            next if !defined $col_def;
1406
1407            $col_def->{name} = $col;
1408
1409            my $db_def = $db_defs->{$col};
1410
1411            if (!$db_def) {
1412                # column is missing altogether; we're going to have to add it
1413                push @cols_to_add, $col;
1414            } else {
1415                if (($col_def->{type} eq 'string')
1416                 && ($db_def->{type} eq 'string')
1417                 && ($col_def->{size} <= $db_def->{size})) {
1418                    if (($col_def->{not_null} || 0) != ($db_def->{not_null} || 0)) {
1419                        push @cols_to_alter, $col;
1420                    }
1421                } elsif ($ddl->type2db($col_def)
1422                      ne $ddl->type2db($db_def)) {
1423                    # types are different
1424                    # don't bother if the database has sufficient
1425                    # capacity for this field
1426                    next if ($db_def->{type} eq 'integer')
1427                         && ($col_def->{type} eq 'smallint'
1428                          || $col_def->{type} eq 'boolean');
1429                    push @cols_to_alter, $col;
1430                } elsif (($col_def->{not_null} || 0) != ($db_def->{not_null} || 0)) {
1431                    push @cols_to_alter, $col;
1432                }
1433            }
1434        }
1435
1436        foreach my $key (keys %$class_idx_defs) {
1437            my $db_idx_def = $db_idx_defs->{$key};
1438            if (!$db_idx_def) {
1439                push @cols_to_index, $key;
1440                next;
1441            }
1442            # if there is a mismatch in definition, add it to index
1443            my $class_idx_def = $class_idx_defs->{$key};
1444            if (ref($class_idx_def)) {
1445                if (!ref $db_idx_def) {
1446                    push @cols_to_index, $key;
1447                }
1448                else {
1449                    my $db_cols;
1450                    if (exists $db_idx_def->{columns}) {
1451                        $db_cols = join ',', @{ $db_idx_def->{columns} };
1452                    }
1453                    else {
1454                        $db_cols = $key;
1455                    }
1456                    my $class_cols;
1457                    if (exists $class_idx_def->{columns}) {
1458                        $class_cols = join ',', @{ $class_idx_def->{columns} };
1459                    }
1460                    else {
1461                        $class_cols = $key;
1462                    }
1463                    if ($db_cols ne $class_cols) {
1464                        push @cols_to_index, $key;
1465                    }
1466                    else {
1467                        if (($db_idx_def->{unique} || 0) != ($class_idx_def->{unique} || 0)) {
1468                            push @cols_to_index, $key;
1469                        }
1470                    }
1471                }
1472            }
1473            else {
1474                if (ref $db_idx_def) {
1475                    push @cols_to_index, $key;
1476                }
1477            }
1478        }
1479    }
1480
1481    if ($fix_class || @cols_to_add || @cols_to_alter || @cols_to_drop || @cols_to_index) {
1482        my %param;
1483        $param{drop} = \@cols_to_drop if @cols_to_drop;
1484        $param{add} = \@cols_to_add if @cols_to_add;
1485        $param{alter} = \@cols_to_alter if @cols_to_alter;
1486        $param{fix} = $fix_class;
1487        $param{index} = \@cols_to_index if @cols_to_index;
1488        if ((@cols_to_add && !$ddl->can_add_column) ||
1489            (@cols_to_alter && !$ddl->can_alter_column) ||
1490            (@cols_to_drop && !$ddl->can_drop_column)) {
1491            $param{fix} = 1;
1492        }
1493        return \%param;
1494    }
1495    undef;
1496}
1497
1498sub seed_database {
1499    my $self = shift;
1500    my (%param) = @_;
1501
1502    require MT::Author;
1503    return undef if MT::Author->exist;
1504
1505    $self->progress($self->translate_escape("Creating initial blog and user records..."));
1506
1507    local $MT::CallbacksEnabled = 1;
1508
1509    require MT::L10N;
1510    my $lang = exists $param{user_lang} ? $param{user_lang} : MT->config->DefaultLanguage;
1511    my $LH = MT::L10N->get_handle($lang);
1512
1513    # TBD: parameter for username/password provided by user from $app
1514    use URI::Escape;
1515    my $author = MT::Author->new;
1516    $author->name(exists $param{user_name} ? uri_unescape($param{user_name}) : 'Melody');
1517    $author->type(MT::Author::AUTHOR());
1518    $author->set_password(exists $param{user_password} ? uri_unescape($param{user_password}) : 'Nelson');
1519    $author->email(exists $param{user_email} ? uri_unescape($param{user_email}) : '');
1520    $author->hint(exists $param{user_hint} ? uri_unescape($param{user_hint}) : '');
1521    $author->nickname(exists $param{user_nickname} ? uri_unescape($param{user_nickname}) : '');
1522    $author->is_superuser(1);
1523    $author->can_create_blog(1);
1524    $author->can_view_log(1);
1525    $author->can_manage_plugins(1);
1526    $author->preferred_language($lang);
1527    $author->external_id(MT::Author->pack_external_id($param{user_external_id})) if exists $param{user_external_id};
1528    $author->auth_type(MT->config->AuthenticationModule);
1529    $author->save or return $self->error($self->translate_escape("Error saving record: [_1].", $author->errstr));
1530    $App->{author} = $author if ref $App;
1531
1532    $self->create_default_roles(%param);
1533
1534    require MT::Blog;
1535    my $blog = MT::Blog->create_default_blog(MT->translate('First Blog'), $param{blog_template_set})
1536        or return $self->error($self->translate_escape("Error saving record: [_1].", MT::Blog->errstr));
1537    $blog->site_path(exists $param{blog_path} ? uri_unescape($param{blog_path}) : '');
1538    $blog->site_url(exists $param{blog_url} ? uri_unescape($param{blog_url}) : '');
1539    $blog->name(exists $param{blog_name} ? uri_unescape($param{blog_name}) : '');
1540    $blog->server_offset(exists $param{blog_timezone} ? ($param{blog_timezone} || 0) : 0);
1541    $blog->template_set($param{blog_template_set});
1542    $blog->save;
1543    MT->run_callbacks( 'blog_template_set_change', { blog => $blog } );
1544
1545    # Create an initial entry and comment for this blog
1546    require MT::Entry;
1547    my $entry = MT::Entry->new;
1548    $entry->blog_id($blog->id);
1549    $entry->title(MT->translate("I just finished installing Movable Type [_1]!", int(MT->product_version)));
1550    $entry->text(MT->translate("Welcome to my new blog powered by Movable Type. This is the first post on my blog and was created for me automatically when I finished the installation process. But that is ok, because I will soon be creating posts of my own!"));
1551    $entry->author_id($author->id);
1552    $entry->status(MT::Entry::RELEASE());
1553    $entry->save
1554        or return $self->error($self->translate_escape("Error saving record: [_1].", MT::Entry->errstr));
1555
1556    require MT::Comment;
1557    my $comment = MT::Comment->new;
1558    $comment->entry_id($entry->id);
1559    $comment->blog_id($blog->id);
1560    $comment->text(MT->translate("Movable Type also created a comment for me as well so that I could see what a comment will look like on my blog once people start submitting comments on all the posts I will write."));
1561    $comment->visible(1);
1562    $comment->junk_status(1);
1563    $comment->author(exists $param{user_nickname} ? uri_unescape($param{user_nickname}) : undef);
1564    $comment->save
1565        or return $self->error($self->translate_escape("Error saving record: [_1].", MT::Comment->errstr));
1566
1567    require MT::Association;
1568    require MT::Role;
1569    my ($blog_admin_role) = MT::Role->load_by_permission("administer_blog");
1570    MT::Association->link( $blog => $blog_admin_role => $author );
1571
1572    1;
1573}
1574
1575## Translation
1576# translate('Blog Administrator')
1577# translate('Can administer the blog.')
1578# translate('Editor')
1579# translate('Can upload files, edit all entries/categories/tags on a blog and publish the blog.')
1580# translate('Author')
1581# translate('Can create entries, edit their own, upload files and publish.')
1582# translate('Designer')
1583# translate('Can edit, manage and publish blog templates.')
1584# translate('Webmaster')
1585# translate('Can manage pages and publish blog templates.')
1586# translate('Contributor')
1587# translate('Can create entries, edit their own and comment.')
1588# translate('Moderator')
1589# translate('Can comment and manage feedback.')
1590# translate('Commenter')
1591# translate('Can comment.')
1592
1593sub create_default_roles {
1594    my $self = shift;
1595    my (%param) = @_;
1596
1597    my @default_roles = (
1598        { name => 'Blog Administrator',
1599          description => 'Can administer the blog.',
1600          role_mask => 2**12,
1601          perms => ['administer_blog'] },
1602        { name => 'Editor',
1603          description => 'Can upload files, edit all entries/categories/tags on a blog and publish the blog.',
1604          perms => ['comment', 'create_post', 'publish_post', 'edit_all_posts', 'edit_categories', 'edit_tags', 'manage_pages',
1605                    'rebuild', 'upload', 'send_notifications', 'manage_feedback'], },
1606        { name => 'Author',
1607          description => 'Can create entries, edit their own, upload files and publish.',
1608          perms => ['comment', 'create_post', 'publish_post', 'upload', 'send_notifications'], },
1609        { name => 'Designer',
1610          description => 'Can edit, manage and publish blog templates.',
1611          role_mask => (2**4 + 2**7),
1612          perms => ['edit_templates', 'rebuild'] },
1613        { name => 'Webmaster',
1614          description => 'Can manage pages and publish blog templates.',
1615          perms => ['manage_pages', 'rebuild'] },
1616        { name => 'Contributor',
1617          description => 'Can create entries, edit their own and comment.',
1618          perms => ['comment', 'create_post'], },
1619        { name => 'Moderator',
1620          description => 'Can comment and manage feedback.',
1621          perms => ['comment', 'manage_feedback'], },
1622        { name => 'Commenter',
1623          description => 'Can comment.',
1624          role_mask => 2**0,
1625          perms => ['comment'], },
1626    );
1627
1628    require MT::Role;
1629    return if MT::Role->exist();
1630
1631    foreach my $r (@default_roles) {
1632        my $role = MT::Role->new();
1633        $role->name(MT->translate($r->{name}));
1634        $role->description(MT->translate($r->{description}));
1635        $role->clear_full_permissions;
1636        $role->set_these_permissions($r->{perms});
1637        if ($r->{name} =~ m/^System/) {
1638            $role->is_system(1);
1639        }
1640        $role->role_mask($r->{role_mask}) if exists $r->{role_mask};
1641        $role->save;
1642    }
1643
1644    1;
1645}
1646
1647sub remove_mtviewphp {
1648    my $self = shift;
1649    my (%param) = @_;
1650
1651    require MT::Template;
1652    $self->progress($self->translate_escape('Removing Dynamic Site Bootstrapper index template...'));
1653    MT::Template->remove( { type => 'index', outfile => 'mtview.php' } );
1654    1;
1655}
1656
1657sub migrate_commenter_auth {
1658    my ($self) = shift;
1659    my (%param) = @_;
1660
1661    my $iter = MT::Blog->load_iter({ 'allow_reg_comments' => 1 });
1662    while (my $blog = $iter->()) {
1663        $blog->commenter_authenticators('TypeKey') if $blog->remote_auth_token;
1664        $blog->save;
1665    }
1666    1;
1667}
1668
1669sub upgrade_templates {
1670    my $self = shift;
1671    my (%opt) = @_;
1672
1673    my $install = $opt{Install} || 0;
1674
1675    my $updated = 0;
1676
1677    my $tmpl_list;
1678    require MT::DefaultTemplates;
1679    $tmpl_list = MT::DefaultTemplates->templates || [];
1680
1681    my $mt = MT->instance;
1682    my @arch_tmpl;
1683
1684    require MT::Template;
1685    require MT::Blog;
1686
1687    my $installer = sub {
1688        my ($val, $blog_id) = @_;
1689
1690        my $terms = {};
1691        $terms->{type} = $val->{type};
1692        $terms->{name} = $val->{name}
1693            if $val->{set} ne 'system';
1694        $terms->{blog_id} = $blog_id;
1695
1696        return 1 if MT::Template->exist( $terms );
1697
1698        $self->progress($self->translate_escape("Creating new template: '[_1]'.", $val->{name}));
1699
1700        my $obj = MT::Template->new;
1701        $obj->build_dynamic(0);
1702        foreach my $v (keys %$val) {
1703            $obj->column($v, $val->{$v}) if $obj->has_column($v);
1704        }
1705        $obj->blog_id($blog_id);
1706        $obj->save or return $self->error($self->translate_escape("Error saving record: [_1].", $obj->errstr));
1707        $updated = 1;
1708        if ($val->{mappings}) {
1709            push @arch_tmpl, {
1710                template => $obj,
1711                mappings => $val->{mappings},
1712            };
1713        }
1714        return 1;
1715    };
1716
1717    for my $val (@$tmpl_list) {
1718        if (!$Installing) {
1719            next if $val->{type} eq 'search_results';
1720        }
1721        if (!$install) {
1722            if (!$val->{global}) {
1723                next if $val->{set} ne 'system';
1724            }
1725        }
1726
1727        my $p = $val->{plugin} || $mt;
1728        $val->{name} = $p->translate($val->{name});
1729        $val->{text} = $p->translate_templatized($val->{text});
1730
1731        if ($val->{global}) {
1732            $installer->($val, 0) or return;
1733        }
1734        else {
1735            my $iter = MT::Blog->load_iter();
1736            while (my $blog = $iter->()) {
1737                $installer->($val, $blog->id);
1738            }
1739        }
1740    }
1741
1742    if (@arch_tmpl) {
1743        $self->progress($self->translate_escape("Mapping templates to blog archive types..."));
1744        require MT::TemplateMap;
1745
1746        for my $map_set (@arch_tmpl) {
1747            my $tmpl = $map_set->{template};
1748            my $mappings = $map_set->{mappings};
1749            foreach my $map_key (keys %$mappings) {
1750                my $m = $mappings->{$map_key};
1751                my $at = $m->{archive_type};
1752                # my $preferred = $mappings->{$map_key}{preferred};
1753                my $map = MT::TemplateMap->new;
1754                $map->archive_type($at);
1755                $map->is_preferred(1);
1756                $map->template_id($tmpl->id);
1757                $map->file_template($m->{file_template}) if $m->{file_template};
1758                $map->blog_id($tmpl->blog_id);
1759                $map->save;
1760            }
1761        }
1762    }
1763
1764    $updated;
1765}
1766
1767sub rename_php_plugin_filenames {
1768    my $self = shift;
1769
1770    my $server_path = MT->instance->server_path() || '';
1771    $server_path =~ s/\/*$//;
1772    my $plugin_path = File::Spec->canonpath("$server_path/php/plugins");
1773
1774    # If PHP plugins directory doesn't exist, return without failing
1775    return 0 if !-d $plugin_path;
1776
1777    opendir(DIR, $plugin_path)
1778        or return 0;
1779    my @files = grep { /^(?:function|block)\.(.*)\.php$/ } readdir(DIR);
1780    closedir(DIR);
1781
1782    return 0 unless @files;
1783
1784    $self->progress($self->translate_escape('Renaming PHP plugin file names...'));
1785    my @error_files = ();
1786    for my $file (@files) {
1787        my $newfile = lc $file;
1788        next if $file eq $newfile;
1789        if (!rename("$plugin_path/$file", "$plugin_path/$newfile")) {
1790            push @error_files, $file;
1791        }
1792    }
1793    if ($#error_files >= 0) {
1794        $self->progress($self->translate_escape('Error renaming PHP files. Please check the Activity Log.'));
1795        MT->log(
1796            {
1797                message => $self->translate_escape("Cannot rename in [_1]: [_2].", $plugin_path, join(', ', @error_files)),
1798                level   => MT::Log::ERROR(),
1799                category => 'upgrade',
1800            }
1801        );
1802    }
1803    1;
1804}
1805
1806sub update_widget_templates {
1807    my $self = shift;
1808
1809    $self->progress($self->translate_escape('Updating widget template records...'));
1810    require MT::Template;
1811    my $iter = MT::Template->load_iter(
1812        { type => 'custom' },
1813        { 'sort' => 'name',
1814          'direction' => 'ascend' }
1815    );
1816
1817    while (my $tmpl = $iter->()) {
1818        my $name = $tmpl->name();
1819        if ($name =~ s/^(?:Widget|Sidebar): ?//) {
1820            $tmpl->name($name);
1821            $tmpl->type('widget');
1822            $tmpl->save;
1823        }
1824    }
1825    1;
1826}
1827
1828sub remove_indexes {
1829    my $self = shift;
1830
1831    $self->progress($self->translate_escape('Removing unnecessary indexes...'));
1832
1833    my $driver = MT::Object->driver;
1834
1835    if ($driver->dbd =~ m/::Pg$|::Oracle$/) {
1836        $driver->sql([
1837            'drop index mt_asset_url',
1838            'drop index mt_asset_file_path',
1839            'drop index mt_blocklist_name',
1840            'drop index mt_entry_blog_id',
1841            'drop index mt_template_build_dynamic'
1842        ]);
1843    } elsif ($driver->dbd =~ m/::mysql$/) {
1844        $driver->sql([
1845            'drop index mt_asset_url on mt_asset',
1846            'drop index mt_asset_file_path on mt_asset',
1847            'drop index mt_blocklist_name on mt_blocklist',
1848            'drop index mt_entry_blog_id on mt_entry',
1849            'drop index mt_template_build_dynamic on mt_tempalte'
1850        ]);
1851    } elsif ($driver->dbd =~ m/::mssqlserver$/) {
1852        $driver->sql([
1853            'drop index mt_asset.mt_asset_url',
1854            'drop index mt_asset.mt_asset_file_path',
1855            'drop index mt_blocklist.mt_blocklist_name',
1856            'drop index mt_entry.mt_entry_blog_id',
1857            'drop index mt_tempalte.mt_template_build_dynamic'
1858        ]);
1859    }
1860    1;
1861}
1862
1863###  Upgrade triggers
1864
1865# we don't need these yet, but it makes me feel good to have them around
1866
1867# 'pre' triggers should execute quickly. 'post' triggers can add steps
1868# if they require processing that will take time to complete.
1869
1870sub pre_upgrade_class { 1 }
1871sub post_upgrade_class { 1 }
1872sub pre_alter_column { 1 }
1873sub post_alter_column { 1 }
1874sub pre_drop_column { 1 }
1875sub post_drop_column { 1 }
1876sub pre_add_column { 1 }
1877sub pre_index_column { 1 }
1878sub post_index_column { 1 }
1879sub pre_schema_upgrade { 1 }
1880
1881# issued last, after all table creation...
1882
1883sub post_schema_upgrade {
1884    my $self = shift;
1885    my ($from) = @_;
1886
1887    my $plugin_ver = MT->config('PluginSchemaVersion') || {};
1888    $plugin_ver->{'core'} = $from;
1889
1890    # run any functions that define a version_limit and where the schema we're
1891    # upgrading from is below that limit.
1892    foreach my $fn (keys %functions) {
1893        my $save_from = $from;
1894        {
1895            my $func = $functions{$fn};
1896
1897            if ($func->{plugin} && (UNIVERSAL::isa($func->{plugin}, 'MT::Component'))) {
1898                my $id = $func->{plugin}->id;
1899                $from = $plugin_ver->{$id};
1900            }
1901            if ($func->{version_limit}
1902                && (defined $from)
1903                && ($from < $func->{version_limit})) {
1904                $self->add_step($fn, from => $from);
1905            }
1906            elsif ($func
1907                && !exists($func->{version_limit})
1908                && !defined($from)) {
1909                $self->add_step($fn);
1910            }
1911        }
1912        $from = $save_from;
1913    }
1914
1915    1;
1916}
1917
1918sub pre_create_table {
1919    my $self = shift;
1920    my ($class) = @_;
1921    $class->driver->dbd->ddl_class->drop_sequence($class);
1922}
1923
1924sub post_create_table {
1925    my $self = shift;
1926    my ($class) = @_;
1927
1928    $class->driver->dbd->ddl_class->create_sequence($class);
1929
1930    if (!$Installing) {
1931        foreach (keys %functions) {
1932            my $func = $functions{$_};
1933            next unless $func->{on_class};
1934            $self->add_step($_) if $func->{on_class} eq $class;
1935        }
1936    }
1937
1938    1;
1939}
1940
1941# Note that this trigger only fires on BerkeleyDB for columns
1942# that are non-null or indexed.
1943
1944sub post_add_column {
1945    my $self = shift;
1946    my ($class, $col_defs) = @_;
1947
1948    if (!$Installing) {
1949        my %cols = map { $_ => 1 } @$col_defs;
1950        foreach (keys %functions) {
1951            my $func = $functions{$_};
1952            next unless $func->{on_field};
1953            if ($func->{on_field} =~ m/^\Q$class\E->(.*)/) {
1954                $self->add_step($_) if $cols{$1};
1955            }
1956        }
1957    }
1958    1;
1959}
1960
1961# Passthru routines-- passing to calling application...
1962
1963sub progress {
1964    my $self = shift;
1965    $App->progress(@_) if $App;
1966}
1967
1968sub translate_escape {
1969    my $self = shift;
1970    my $trans = MT->translate(@_);
1971    return $trans if $CLI;
1972    $trans = MT::I18N::encode_text($trans, undef, 'utf-8');
1973    return MT::Util::escape_unicode($trans);
1974}
1975
1976sub error {
1977    my $self = shift;
1978    my ($msg) = @_;
1979    $App->error(@_) if $App;
1980    return undef;
1981}
1982
1983sub add_step {
1984    my $self = shift;
1985    if ($App && (ref $App)) {
1986        $App->add_step(@_);
1987    } else {
1988        push @steps, [ @_ ];
1989    }
1990}
1991
1992# Misc utilities.
1993
1994sub detect_schema_version {
1995    my $self = shift;
1996
1997    require MT::Object;
1998    my $driver = MT::Object->driver;
1999
2000    require MT::Config;
2001    if ($driver->table_exists('MT::Config')) {
2002        return 3.2;
2003    }
2004
2005    require MT::Template;
2006    my $dyn_error_template =
2007        MT::Template->exist({type => 'dynamic_error'});
2008    if ($dyn_error_template) {
2009        return 3.1;
2010    }
2011
2012    my $comment_pending_template =
2013        MT::Template->exist({type => 'comment_pending'});
2014    if ($comment_pending_template) {
2015        return 3.0;
2016    }
2017
2018    require MT::TemplateMap;
2019    if ($driver->table_exists('MT::TemplateMap')) {
2020        return 2.0;
2021    }
2022
2023    1.0;
2024}
2025
2026# A note about upgrade routines:
2027#
2028# They should all be 'safe' to execute, regardless of the
2029# active schema. In other words, running them twice in a row
2030# should not cause any errors or break the schema.
2031
2032sub core_fix_type {
2033    my $self = shift;
2034    my (%param) = @_;
2035
2036    my $type = $param{type};
2037    my $class = MT->model($type);
2038
2039    my $result = $self->type_diff($type);
2040    return 1 unless $result;
2041    return 1 unless $result->{fix};
2042
2043    my $alter = $result->{alter};
2044    my $add = $result->{add};
2045    my $drop = $result->{drop};
2046    my $index = $result->{index};
2047
2048    my $driver = $class->driver;
2049    my $ddl = $driver->dbd->ddl_class;
2050    my @stmts;
2051    push @stmts, sub { $self->pre_upgrade_class($class) };
2052    push @stmts, $ddl->upgrade_begin($class);
2053    push @stmts, sub { $self->pre_create_table($class) };
2054    push @stmts, sub { $self->pre_add_column($class, $add) } if $add;
2055    push @stmts, sub { $self->pre_alter_column($class, $alter) } if $alter;
2056    push @stmts, sub { $self->pre_drop_column($class, $drop) } if $drop;
2057    push @stmts, sub { $self->pre_index_column($class, $index) } if $index;
2058    push @stmts, $ddl->fix_class($class);
2059    push @stmts, sub { $self->post_create_table($class) };
2060    push @stmts, sub { $self->post_add_column($class, $add) } if $add;
2061    push @stmts, sub { $self->post_alter_column($class, $alter) } if $alter;
2062    push @stmts, sub { $self->post_drop_column($class, $drop) } if $drop;
2063    push @stmts, sub { $self->post_index_column($class, $index) } if $index;
2064    push @stmts, $ddl->upgrade_end($class);
2065    push @stmts, sub { $self->post_upgrade_class($class) };
2066    $self->run_statements($class, @stmts);
2067}
2068
2069sub core_column_action {
2070    my $self = shift;
2071    my ($action, %param) = @_;
2072
2073    my $type = $param{type};
2074    my $class = MT->model($type);
2075    my $defs = $class->column_defs;
2076
2077    my $result = $self->type_diff($type);
2078    return 1 unless $result;
2079    my $columns = $result->{$action};
2080    return 1 unless $columns;
2081
2082    my $pre_method = "pre_${action}_column";
2083    my $post_method = "post_${action}_column";
2084    my $method = "${action}_column";
2085
2086    my $driver = $class->driver;
2087    my $ddl = $driver->dbd->ddl_class;
2088    my @stmts;
2089    push @stmts, sub { $self->pre_upgrade_class($class) };
2090    push @stmts, $ddl->upgrade_begin($class);
2091    push @stmts, sub { $self->$pre_method($class, $columns) };
2092    push @stmts, $ddl->$method($class, $_) foreach @$columns;
2093    push @stmts, sub { $self->$post_method($class, $columns) };
2094    push @stmts, $ddl->upgrade_end($class);
2095    push @stmts, sub { $self->post_upgrade_class($class) };
2096    $self->run_statements($class, @stmts);
2097}
2098
2099sub run_statements {
2100    my $self = shift;
2101    my ($class, @stmts) = @_;
2102
2103    my $driver = $class->driver;
2104    my $defs = $class->column_defs;
2105    my $dbh = $driver->rw_handle;
2106    my $mt = MT->instance;
2107
2108    my $updated = 0;
2109    if (@stmts) {
2110        $self->progress($self->translate_escape("Upgrading table for [_1] records...", $class->can('class_label') ? $class->class_label : $class));
2111        eval {
2112            foreach my $stmt (@stmts) {
2113                if (ref $stmt eq 'CODE') {
2114                    $stmt->() if !$DryRun;
2115                } else {
2116                    if ($dbh && !$DryRun) {
2117                        my $err;
2118                        $dbh->do($stmt) or $err = $dbh->errstr;
2119                        if ($err) {
2120                            # ignore drop errors; the table/sequence/constraint
2121                            # didn't exist
2122                            if (($stmt !~ m/^drop /i) && ($stmt !~ m/DROP CONSTRAINT /i)) {
2123                                die "failed to execute statement $stmt: $err";
2124                            }
2125                        }
2126                    } elsif ($dbh && $DryRun) {
2127                        $self->run_callbacks('SQL', $stmt);
2128                    }
2129                }
2130                $updated = 1;
2131            }
2132        };
2133        if ($@) {
2134            return $self->error($@);
2135        }
2136    }
2137    $updated;
2138}
2139
2140sub core_upgrade_begin {
2141    my $self = shift;
2142    my (%param) = @_;
2143    my $from_schema = $param{from};
2144    if ($from_schema) {
2145        my $cur_schema = MT->schema_version;
2146        $self->progress($self->translate_escape("Upgrading database from version [_1].", $from_schema)) if $from_schema < $cur_schema;
2147        $self->pre_schema_upgrade($from_schema);
2148    }
2149}
2150
2151sub core_upgrade_end {
2152    my $self = shift;
2153    my (%param) = @_;
2154
2155    my $from_schema = $param{from};
2156    if ($from_schema) {
2157        $self->post_schema_upgrade($from_schema);
2158    }
2159    1;
2160}
2161
2162sub core_finish {
2163    my $self = shift;
2164
2165    my $user;
2166    if ((ref $App) && ($App->{author})) {
2167        $user = $App->{author};
2168    }
2169
2170    my $cfg = MT->config;
2171    my $cur_schema = MT->instance->schema_version;
2172    my $old_schema = $cfg->SchemaVersion || 0;
2173    if ($cur_schema > $old_schema) {
2174        $self->progress($self->translate_escape("Database has been upgraded to version [_1].", $cur_schema)) ;
2175        if ($user && !$DryRun) {
2176            MT->log(MT->translate("User '[_1]' upgraded database to version [_2]", $user->name, $cur_schema));
2177        }
2178        $cfg->SchemaVersion( $cur_schema, 1 );
2179    }
2180
2181    my $plugin_schema = $cfg->PluginSchemaVersion || {};
2182    foreach my $plugin (@MT::Components) {
2183        my $ver = $plugin->schema_version;
2184        next unless $ver;
2185        next if $plugin->id eq 'core';
2186        my $old_plugin_schema = $plugin_schema->{$plugin->id} || 0;
2187        if ($old_plugin_schema && ($ver > $old_plugin_schema)) {
2188            $self->progress($self->translate_escape("Plugin '[_1]' upgraded successfully to version [_2] (schema version [_3]).", $plugin->label, $plugin->version || '-', $ver));
2189            if ($user && !$DryRun) {
2190                MT->log(MT->translate("User '[_1]' upgraded plugin '[_2]' to version [_3] (schema version [_4]).", $user->name, $plugin->label, $plugin->version || '-', $ver));
2191            }
2192        } elsif ($ver && !$old_plugin_schema) {
2193            $self->progress($self->translate_escape("Plugin '[_1]' installed successfully.", $plugin->label));
2194            if ($user && !$DryRun) {
2195                MT->log(MT->translate("User '[_1]' installed plugin '[_2]', version [_3] (schema version [_4]).", $user->name, $plugin->label, $plugin->version || '-', $ver));
2196            }
2197        }
2198        $plugin_schema->{$plugin->id} = $ver;
2199    }
2200    if (keys %$plugin_schema) {
2201        $cfg->PluginSchemaVersion($plugin_schema, 1);
2202    }
2203
2204    my $cur_version = MT->version_number;
2205    if ( !defined($cfg->MTVersion) || ( $cur_version > $cfg->MTVersion ) ) {
2206        $cfg->MTVersion( $cur_version, 1 );
2207    }
2208    $cfg->save_config unless $DryRun;
2209
2210    # do one last thing....
2211    if ((ref $App) && ($App->can('finish'))) {
2212        $App->finish();
2213    }
2214
2215    1;
2216}
2217
2218sub core_set_superuser {
2219    my $self = shift;
2220
2221    my $app = $App;
2222    my $author;
2223    if ((ref $app) && ($app->{author})) {
2224        require MT::Author;
2225        $self->progress($self->translate_escape("Setting your permissions to administrator."));
2226        $author = MT::Author->load($app->{author}->id);
2227    } elsif ($SuperUser) {
2228        require MT::Author;
2229        $self->progress($self->translate_escape("Setting your permissions to administrator."));
2230        $author = MT::Author->load($SuperUser);
2231    }
2232
2233    if ($author) {
2234        $author->is_superuser(1);
2235        $author->save or return $self->error($self->translate_escape("Error saving record: [_1].", $author->errstr));
2236    }
2237
2238    1;
2239}
2240
2241sub core_remove_unique_constraints {
2242    my $self = shift;
2243
2244    my $driver = MT::Object->driver;
2245    if (ref $driver->dbd =~ m/::Pg$/) {
2246        # category, author, permission, template
2247        $driver->sql([
2248            'alter table mt_category drop constraint mt_category_category_blog_id_key',
2249            'create index mt_category_label on mt_category (category_label)',
2250
2251            'alter table mt_author drop constraint mt_author_author_name_key',
2252            'create index mt_author_name on mt_author (author_name)',
2253            'alter table mt_permission drop constraint mt_permission_permission_blog_id_key',
2254            'create index mt_permission_blog_id on mt_permission (permission_blog_id)',
2255            'alter table mt_template drop constraint mt_template_template_blog_id_key',
2256            'create index mt_template_blog_id on mt_template (template_blog_id)'
2257        ]);
2258    } elsif (ref $driver->dbd =~ m/::mysql$/) {
2259        $driver->sql([
2260            'alter table mt_category drop index category_blog_id',
2261            'create index category_blog_id on mt_category (category_blog_id)',
2262            'create index category_label on mt_category (category_label)',
2263            'alter table mt_author drop index author_name',
2264            'create index author_name on mt_author (author_name)',
2265            'alter table mt_permission drop index permission_blog_id',
2266            'create index permission_blog_id on mt_permission (permission_blog_id)',
2267            'alter table mt_template drop index template_blog_id',
2268            'create index template_blog_id on mt_template (template_blog_id)'
2269        ]);
2270    }
2271    1;
2272}
2273
2274sub _merge_comment_response_templates_updater {
2275    my ($blog) = @_;
2276    require MT::Template;
2277    my $tmpl = MT::Template->load({ blog_id => $blog->id, type => 'comment_response' });
2278    unless ($tmpl) {
2279        $tmpl = new MT::Template;
2280        $tmpl->blog_id($blog->id);
2281        $tmpl->type('comment_response');
2282    }
2283
2284    my $confirm_template = <<'EOT';
2285<MTSetVarBlock name="page_title"><__trans phrase="Comment Posted"></MTSetVarBlock>
2286
2287<MTSetVar name="heading" value="<__trans phrase="Confirmation...">">
2288
2289<MTSetVarBlock name="message">
2290<p><__trans phrase="Your comment has been posted!"></p>
2291</MTSetVarBlock>
2292EOT
2293
2294    my $pending_template = <<'EOT';
2295<MTSetVarBlock name="page_title"><__trans phrase="Comment Pending"></MTSetVarBlock>
2296
2297<MTSetVar name="heading" value="<__trans phrase="Thank you for commenting.">">
2298
2299<MTSetVarBlock name="message">
2300<p><__trans phrase="Your comment has been received and held for approval by the blog owner."></p>
2301</MTSetVarBlock>
2302EOT
2303
2304    my $error_template = <<'EOT';
2305<MTSetVarBlock name="page_title"><__trans phrase="Comment Submission Error"></MTSetVarBlock>
2306
2307<MTSetVar name="heading" value="$page_title">
2308
2309<MTSetVarBlock name="message">
2310<p><__trans phrase="Your comment submission failed for the following reasons:"></p>
2311<blockquote>
2312    <$MTErrorMessage$>
2313</blockquote>
2314</MTSetVarBlock>
2315EOT
2316
2317    my $header_template = <<'EOT';
2318<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2319    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2320<html xmlns="http://www.w3.org/1999/xhtml" id="sixapart-standard">
2321<head>
2322    <meta http-equiv="Content-Type" content="text/html; charset=<$MTPublishCharset$>" />
2323    <meta name="generator" content="<$MTProductName version="1"$>" />
2324    <link rel="stylesheet" href="<$MTBlogURL$>styles-site.css" type="text/css" />
2325    <title>
2326    <__trans phrase="[_1]: [_2]" params="<$MTBlogName encode_html="1"$>%%<$MTGetVar name="page_title"$>">
2327    </title>
2328    <script type="text/javascript" src="<$MTBlogURL$>mt-site.js"></script>
2329</head>
2330<body class="layout-one-column comment-preview" onload="individualArchivesOnLoad(commenter_name)">
2331    <div id="container">
2332        <div id="container-inner" class="pkg">
2333            <div id="banner">
2334                <div id="banner-inner" class="pkg">
2335                    <h1 id="banner-header"><a href="<$MTBlogURL$>" accesskey="1"><$MTBlogName encode_html="1"$></a></h1>
2336                    <h2 id="banner-description"><$MTBlogDescription$></h2>
2337                </div>
2338            </div>
2339            <div id="pagebody">
2340                <div id="pagebody-inner" class="pkg">
2341                    <div id="alpha">
2342                        <div id="alpha-inner" class="pkg">
2343EOT
2344
2345    my $footer_template = <<'EOT';
2346                        </div>
2347                    </div>
2348                </div>
2349            </div>
2350        </div>
2351    </div>
2352</body>
2353</html>
2354EOT
2355
2356    my $message_template = <<'EOT';
2357<h1><$MTGetVar name="heading"$></h1>
2358
2359<$MTGetVar name="message"$>
2360
2361<p><__trans phrase="Return to the <a href="[_1]">original entry</a>." params="<$MTEntryLink$>"></p>
2362EOT
2363
2364    my $mt = MT->instance;
2365    $tmpl->name($mt->translate("Comment Response"));
2366    $tmpl->text($mt->translate_templatized(<<"EOT"));
2367<MTSetVar name="system_template" value="1">
2368<MTSetVar name="feedback_template" value="1">
2369
2370<MTIf name="body_class" eq="mt-comment-pending">
2371$pending_template
2372</MTIf>
2373
2374<MTIf name="body_class" eq="mt-comment-error">
2375$error_template
2376</MTIf>
2377
2378<MTIf name="body_class" eq="mt-comment-confirmation">
2379$confirm_template
2380</MTIf>
2381
2382$header_template
2383
2384$message_template
2385
2386$footer_template
2387EOT
2388    $tmpl->save;
2389}
2390
2391sub core_create_config_table {
2392    my $self = shift;
2393
2394    require MT::Config;
2395    my $config = MT::Config->load();
2396    if (!$config) {
2397        #$self->progress($self->translate_escape("Creating configuration record."));
2398        $config = MT::Config->new;
2399        $config->data('');
2400        $config->save or return $self->error($self->translate_escape("Error saving record: [_1].", $config->errstr));
2401    }
2402    return 1;
2403}
2404
2405sub core_set_enable_archive_paths {
2406    my $self = shift;
2407    MT->config->EnableArchivePaths(1, 1);
2408    return 1;
2409}
2410
2411sub core_create_template_maps {
2412    my $self = shift;
2413    my (%param) = @_;
2414   
2415    my $offset = $param{offset};
2416    require MT::Template;
2417    require MT::TemplateMap;
2418    require MT::Blog;
2419
2420    my $msg = $self->translate_escape("Creating template maps...");
2421    if ($offset) {
2422        my $count = MT::Template->count;
2423        return 1 unless $count;
2424        $self->progress(sprintf("$msg (%d%%)", ($offset / $count * 100)), 1);
2425    } else {
2426        $self->progress($msg, 1);
2427    }
2428
2429    my $iter = MT::Template->load_iter(undef, { offset => $offset, limit => $MAX_ROWS+1 });
2430    my $start = time;
2431    my $continue = 0;
2432    my $count = 0;
2433    while (my $tmpl = $iter->()) {
2434        $offset++;
2435        my $blog = MT::Blog->load($tmpl->blog_id);
2436        my(@at);
2437        if ($tmpl->type eq 'archive') {
2438            @at = qw( Daily Weekly Monthly );
2439        } elsif ($tmpl->type eq 'category') {
2440            @at = ('Category');
2441        } elsif ($tmpl->type eq 'page') {
2442            @at = ('Page');
2443        } elsif ($tmpl->type eq 'individual') {
2444            @at = ('Individual');
2445        } else {
2446            next;
2447        }
2448        for my $at (@at) {
2449            my $meth = 'archive_tmpl_' . lc($at);
2450            my $file_tmpl = $blog->$meth();
2451            my $existing = MT::TemplateMap->load({ blog_id => $blog->id,
2452                archive_type => $at, template_id => $tmpl->id });
2453            if (!$existing) {
2454                my $map = MT::TemplateMap->new;
2455                if ($file_tmpl) {
2456                    $self->progress($self->translate_escape("Mapping template ID [_1] to [_2] ([_3]).", $tmpl->id, $at, $file_tmpl));
2457                    $map->file_template($file_tmpl);
2458                } else {
2459                    $self->progress($self->translate_escape("Mapping template ID [_1] to [_2].", $tmpl->id, $at));
2460                }
2461                $map->archive_type($at);
2462                $map->is_preferred(1);
2463                $map->template_id($tmpl->id);
2464                $map->blog_id($tmpl->blog_id);
2465                $map->save or return $self->error($self->translate_escape("Error saving record: [_1].", $map->errstr));
2466            }
2467        }
2468        $count++;
2469        $continue = 1, last if $count == $MAX_ROWS;
2470        $continue = 1, last if time > $start + $MAX_TIME;
2471    }
2472    if ($continue) {
2473        $iter->('finish');
2474        return $offset;
2475    } else {
2476        $self->progress("$msg (100%)", 1);
2477    }
2478    1;
2479}
2480
2481sub core_update_records {
2482    my $self = shift;
2483    my (%param) = @_;
2484
2485    my $class = MT->model($param{type});
2486    return $self->error($self->translate_escape("Error loading class: [_1].", $param{type}))
2487        unless $class;
2488
2489    my $msg;
2490    my $class_label = ($class->can('class_label') ? $class->class_label : $class);
2491    if ($param{label}) {
2492        $msg = $param{label};
2493        if (ref $msg eq 'CODE') {
2494            $msg = $msg->($class_label);
2495        }
2496        $msg = $self->translate_escape($msg);
2497    } else {
2498        $msg = $self->translate_escape($param{message} || "Updating [_1] records...", $class_label);
2499    }
2500    my $offset = $param{offset};
2501    if ($offset) {
2502        my $count = $class->count;
2503        return unless $count;
2504        $self->progress(sprintf("$msg (%d%%)", ($offset/$count*100)), $param{step});
2505    } else {
2506        $self->progress($msg, $param{step});
2507    }
2508
2509    my $cond = MT->handler_to_coderef($param{condition});
2510    my $code = MT->handler_to_coderef($param{code});
2511    my $sql = $param{sql};
2512
2513    my $continue = 0;
2514    my $driver = $class->driver;
2515
2516    if ($sql && $DryRun) {
2517        $self->run_callbacks('SQL', $sql);
2518    }
2519    return 1 if $DryRun;
2520
2521    if (!$sql || !$driver->sql($sql)) {
2522        my $iter= $class->load_iter(undef, { offset => $offset, limit => $MAX_ROWS+1 });
2523        my $start = time;
2524        my @list;
2525        while (my $obj = $iter->()) {
2526            push @list, $obj;
2527            $continue = 1, last if scalar @list == $MAX_ROWS;
2528        }
2529        $iter->('finish') if $continue;
2530        for my $obj (@list) {
2531            $offset++;
2532            if ($cond) {
2533                next unless $cond->($obj, %param);
2534            }
2535            $code->($obj);
2536            use Data::Dumper;
2537            $obj->save()
2538                or return $self->error($self->translate_escape("Error saving [_1] record # [_3]: [_2]... [_4].", $class_label, $obj->errstr, $obj->id, Dumper($obj)));
2539            $continue = 1, last if time > $start + $MAX_TIME;
2540        }
2541    }
2542    if ($continue) {
2543        return $offset;
2544    } else {
2545        $self->progress("$msg (100%)", $param{step});
2546    }
2547    1;
2548}
2549
2550#############################################################################
2551
2552{
2553    my %HighASCII = (
2554        "\xc0" => 'A',    # A`
2555        "\xe0" => 'a',    # a`
2556        "\xc1" => 'A',    # A'
2557        "\xe1" => 'a',    # a'
2558        "\xc2" => 'A',    # A^
2559        "\xe2" => 'a',    # a^
2560        "\xc4" => 'Ae',   # A:
2561        "\xe4" => 'ae',   # a:
2562        "\xc3" => 'A',    # A~
2563        "\xe3" => 'a',    # a~
2564        "\xc8" => 'E',    # E`
2565        "\xe8" => 'e',    # e`
2566        "\xc9" => 'E',    # E'
2567        "\xe9" => 'e',    # e'
2568        "\xca" => 'E',    # E^
2569        "\xea" => 'e',    # e^
2570        "\xcb" => 'Ee',   # E:
2571        "\xeb" => 'ee',   # e:
2572        "\xcc" => 'I',    # I`
2573        "\xec" => 'i',    # i`
2574        "\xcd" => 'I',    # I'
2575        "\xed" => 'i',    # i'
2576        "\xce" => 'I',    # I^
2577        "\xee" => 'i',    # i^
2578        "\xcf" => 'Ie',   # I:
2579        "\xef" => 'ie',   # i:
2580        "\xd2" => 'O',    # O`
2581        "\xf2" => 'o',    # o`
2582        "\xd3" => 'O',    # O'
2583        "\xf3" => 'o',    # o'
2584        "\xd4" => 'O',    # O^
2585        "\xf4" => 'o',    # o^
2586        "\xd6" => 'Oe',   # O:
2587        "\xf6" => 'oe',   # o:
2588        "\xd5" => 'O',    # O~
2589        "\xf5" => 'o',    # o~
2590        "\xd8" => 'Oe',   # O/
2591        "\xf8" => 'oe',   # o/
2592        "\xd9" => 'U',    # U`
2593        "\xf9" => 'u',    # u`
2594        "\xda" => 'U',    # U'
2595        "\xfa" => 'u',    # u'
2596        "\xdb" => 'U',    # U^
2597        "\xfb" => 'u',    # u^
2598        "\xdc" => 'Ue',   # U:
2599        "\xfc" => 'ue',   # u:
2600        "\xc7" => 'C',    # ,C
2601        "\xe7" => 'c',    # ,c
2602        "\xd1" => 'N',    # N~
2603        "\xf1" => 'n',    # n~
2604        "\xdf" => 'ss',
2605    );
2606    my $HighASCIIRE = join '|', keys %HighASCII;
2607    sub mt32_convert_high_ascii {
2608        my($s) = @_;
2609        $s =~ s/($HighASCIIRE)/$HighASCII{$1}/g;
2610        $s;
2611    }
2612}
2613
2614sub mt32_iso_dirify {
2615    my $s = $_[0];
2616    my $sep;
2617    if ($_[1] && ($_[1] ne '1')) {
2618        $sep = $_[1];
2619    } else {
2620        $sep = '_';
2621    }
2622    $s = mt32_convert_high_ascii($s);  ## convert high-ASCII chars to 7bit.
2623    $s = lc $s;                   ## lower-case.
2624    $s = MT::Util::remove_html($s);         ## remove HTML tags.
2625    $s =~ s!&[^;\s]+;!!g;         ## remove HTML entities.
2626    $s =~ s![^\w\s]!!g;           ## remove non-word/space chars.
2627    $s =~ s! +!$sep!g;             ## change space chars to underscores.
2628    $s;   
2629}
2630
2631sub mt32_utf8_dirify {
2632    my $s = $_[0];
2633    my $sep;
2634    if ($_[1] && ($_[1] ne '1')) {
2635        $sep = $_[1];
2636    } else {
2637        $sep = '_';
2638    }
2639    $s = mt32_xliterate_utf8($s);      ## convert two-byte UTF-8 chars to 7bit ASCII
2640    $s = lc $s;                   ## lower-case.
2641    $s = MT::Util::remove_html($s);         ## remove HTML tags.
2642    $s =~ s!&[^;\s]+;!!g;         ## remove HTML entities.
2643    $s =~ s![^\w\s]!!g;           ## remove non-word/space chars.
2644    $s =~ s! +!$sep!g;             ## change space chars to underscores.
2645    $s;   
2646}
2647
2648sub mt32_dirify {
2649    ($MT::VERSION && MT->instance->{cfg}->PublishCharset =~ m/utf-?8/i)
2650        ? mt32_utf8_dirify(@_) : mt32_iso_dirify(@_);
2651}
2652
2653sub mt32_xliterate_utf8 {
2654    my ($str) = @_;
2655    my %utf8_table = (
2656          "\xc3\x80" => 'A',    # A`
2657          "\xc3\xa0" => 'a',    # a`
2658          "\xc3\x81" => 'A',    # A'
2659          "\xc3\xa1" => 'a',    # a'
2660          "\xc3\x82" => 'A',    # A^
2661          "\xc3\xa2" => 'a',    # a^
2662          "\xc3\x84" => 'Ae',   # A:
2663          "\xc3\xa4" => 'ae',   # a:
2664          "\xc3\x83" => 'A',    # A~
2665          "\xc3\xa3" => 'a',    # a~
2666          "\xc3\x88" => 'E',    # E`
2667          "\xc3\xa8" => 'e',    # e`
2668          "\xc3\x89" => 'E',    # E'
2669          "\xc3\xa9" => 'e',    # e'
2670          "\xc3\x8a" => 'E',    # E^
2671          "\xc3\xaa" => 'e',    # e^
2672          "\xc3\x8b" => 'Ee',   # E:
2673          "\xc3\xab" => 'ee',   # e:
2674          "\xc3\x8c" => 'I',    # I`
2675          "\xc3\xac" => 'i',    # i`
2676          "\xc3\x8d" => 'I',    # I'
2677          "\xc3\xad" => 'i',    # i'
2678          "\xc3\x8e" => 'I',    # I^
2679          "\xc3\xae" => 'i',    # i^
2680          "\xc3\x8f" => 'Ie',   # I:
2681          "\xc3\xaf" => 'ie',   # i:
2682          "\xc3\x92" => 'O',    # O`
2683          "\xc3\xb2" => 'o',    # o`
2684          "\xc3\x93" => 'O',    # O'
2685          "\xc3\xb3" => 'o',    # o'
2686          "\xc3\x94" => 'O',    # O^
2687          "\xc3\xb4" => 'o',    # o^
2688          "\xc3\x96" => 'Oe',   # O:
2689          "\xc3\xb6" => 'oe',   # o:
2690          "\xc3\x95" => 'O',    # O~
2691          "\xc3\xb5" => 'o',    # o~
2692          "\xc3\x98" => 'Oe',   # O/
2693          "\xc3\xb8" => 'oe',   # o/
2694          "\xc3\x99" => 'U',    # U`
2695          "\xc3\xb9" => 'u',    # u`
2696          "\xc3\x9a" => 'U',    # U'
2697          "\xc3\xba" => 'u',    # u'
2698          "\xc3\x9b" => 'U',    # U^
2699          "\xc3\xbb" => 'u',    # u^
2700          "\xc3\x9c" => 'Ue',   # U:
2701          "\xc3\xbc" => 'ue',   # u:
2702          "\xc3\x87" => 'C',    # ,C
2703          "\xc3\xa7" => 'c',    # ,c
2704          "\xc3\x91" => 'N',    # N~
2705          "\xc3\xb1" => 'n',    # n~
2706          "\xc3\x9f" => 'ss',   # double-s
2707    );
2708   
2709    $str =~ s/([\200-\377]{2})/$utf8_table{$1}||''/ge;
2710    $str;
2711}
2712
27131;
2714__END__
2715
2716=head1 NAME
2717
2718MT::Upgrade - MT class for managing system upgrades.
2719
2720=head1 SYNOPSIS
2721
2722    MT::Upgrade->do_upgrade(Install => 1);
2723
2724=head1 DESCRIPTION
2725
2726This module is responsible for handling the upgrade or installation of
2727an MT database. The framework is flexible enough for third party plugins
2728to use as well to manage their own schema (please refer to the documentation
2729in L<MT::Plugin> for more information on this).
2730
2731=head1 METHODS
2732
2733=head2 MT::Upgrade-E<gt>do_upgrade
2734
2735The main worker method for this module is I<do_upgrade>. It accepts a
2736handful of arguments, which are:
2737
2738=over 4
2739
2740=item * Install
2741
2742Specify a value of '1' to assume a new installation along with an operation
2743to install a blog and initial user.
2744
2745=item * App
2746
2747A package name or app object that can service the following methods:
2748
2749=over 4
2750
2751=item * progress($package, $message)
2752
2753Called during the upgrade operation to provide feedback with respect to
2754the operations the upgrade process is running.
2755
2756=item * error($package, $message)
2757
2758Called during the upgrade operation to communicate an error that has
2759occurred.
2760
2761=item * translate_escape
2762
2763Call this method to translate messages and phrases which are to appear
2764on the progress screen.  DO NOT use this method to messages and phrases
2765which directly are stored in database.  Use MT->translate for the purpose.
2766
2767=back
2768
2769=item * CLI
2770
2771Specified (set to '1') when invoked from a command line tool. This prevents
2772encoding response messages in the configured PublishCharset for the
2773installation.
2774
2775=item * SuperUser
2776
2777If upgrading from the command line, and running on a pre-MT 3.2 database,
2778set this to an existing author ID that should be upgrade to system
2779administrator status.
2780
2781=item * DryRun
2782
2783Specified (set to '1') to examine the database for installation/upgrade
2784needs but not actually make any physical changes to the database. This will
2785issue all the upgrade progress messages without doing the upgrade itself.
2786
2787=back
2788
2789=head1 CALLBACKS
2790
2791The upgrade module defines the following MT callbacks:
2792
2793=over 4
2794
2795=item * MT::Upgrade::SQL
2796
2797Called with each SQL statement that is executed against the database
2798as part of the upgrade process. The parameters passed to this callback are:
2799
2800    $callback, $upgrade_app, $sql_statement
2801
2802The first parameter is an L<MT::Callback> object. C<$upgrade_app> is a
2803package name or L<MT::App> object used to drive the upgrade process.
2804C<$sql_statement> is the actual SQL query that is about to be executed
2805against the database.
2806
2807=back
2808
2809=head1 UPGRADE FUNCTIONS
2810
2811The bulk of this module consists of Movable Type upgrade operations.
2812These are declared as upgrade functions, and are registered in the
2813package variabled '%functions'. (Note: the word 'function' here is
2814not meant to describe a Perl subroutine.)
2815
2816Some functions are invoked to manage the upgrade process from start
2817to finish ('core_upgrade_begin' for instance, which merely displays
2818a progress message to the calling application). The rest handle
2819schema and data transformation from one version of the MT schema to
2820another.
2821
2822Schema translation itself is handled by Movable Type automatically.
2823MT is able to check the physical schema represenation in the database
2824and compare it with the schema as defined by the L<MT::Object>-descended
2825package. If a new property is added to the L<MT::Blog> package, the
2826upgrade process sees that has happened and can issue the actual
2827'alter table' SQL statement necessary to add it to the database. The
2828'core_fix_type' function is responsible for examining a particular
2829table used by a class like L<MT::Blog> and will append additional
2830upgrade steps ('core_add_column', 'core_alter_column') that it finds
2831necessary to the upgrade workflow.
2832
2833Following the schema translation operations, the data transformation
2834functions would be used to manipulate the data as necessary from
2835an older schema to the current one. For instance, the
2836'core_create_placements' upgrade function was written to upgrade
2837really old MT schemas from the pre-2.0 release to the current schema.
2838The upgrade function is registered like this:
2839
2840    $MT::Upgrade::functions{core_create_placements} = {
2841        version_limit => 2.0,
2842        code          => \&core_update_records,
2843        priority      => 9.1,
2844        updater       => {
2845            class     => 'MT::Entry',
2846            message   => 'Creating entry category placements...',
2847            condition => sub { $_[0]->category_id },
2848            code      => sub {
2849                require MT::Placement;
2850                my $entry = shift;
2851                my $existing = MT::Placement->load({ entry_id => $entry->id,
2852                    category_id => $entry->category_id });
2853                if (!$existing) {
2854                    my $place = MT::Placement->new;
2855                    $place->entry_id($entry->id);
2856                    $place->blog_id($entry->blog_id);
2857                    $place->category_id($entry->category_id);
2858                    $place->is_primary(1);
2859                    $place->save;
2860                }
2861                $entry->category_id(0);
2862            }
2863        }
2864    };
2865
2866With MT version 2.0, the L<MT::Placement> class was introduced and
2867immediately deprecated the use of MT::Entry-E<gt>category as a result.
2868To facilitate upgrading the existing L<MT::Entry> objects this upgrade
2869function is declared such that:
2870
2871=over 4
2872
2873=item * It is limited to only run for MT schemas older than version 2.0 (the version_limit element handles this).
2874
2875=item * It operates on L<MT::Entry> objects (updater-E<gt>class element
2876declares that).
2877
2878=item * It tells the user what is happening (updater-E<gt>message).
2879
2880=item * It excludes any L<MT::Entry> objects that do not have a category_id element (updater-E<gt>condition).
2881
2882=item * It checks for an existing L<MT::Placement> relationship; if not
2883present, it creates one (updater-E<gt>code).
2884
2885=item * It empties out the category_id member of the L<MT::Entry> object
2886being upgrade to prevent it from being processed in the future
2887(updater-E<gt>code).
2888
2889=back
2890
2891For plugins, upgrade functions are assignable in the plugin registration
2892hash as documented in L<MT::Plugin>. You may also return a hashref of
2893upgrade functions from the plugin using the MT::Plugin::upgrade_functions
2894subroutine.
2895
2896Let's look at the anatomy of an upgrade function declaration:
2897
2898=over 4
2899
2900=item * version_limit (optional)
2901
2902The version_limit property allows you to declare that this upgrade
2903operation is only applicable to MT B<schema> versions below the version
2904specified.
2905
2906To register an upgrade function that is only applied to releases prior
2907to the current one, specify the current schema version as the version
2908limit. This will allow the upgrade function to run for any prior releases
2909but prevent it from running in subsequent releases.
2910
2911B<NOTE>: If you are declaring a B<plugin> upgrade function, this version
2912limit is compared with your plugin's schema version, not the Movable Type
2913schema version.
2914
2915=item * priority (optional)
2916
2917If your upgrade operation is dependent on another being done already,
2918it is possible to order them using the priority value. A lower value
2919means a higher priority.
2920
2921=item * condition (optional)
2922
2923This is a coderef parameter. If specified, it should return a true or
2924false value that determines whether the upgrade step is actually to run
2925or not.
2926
2927When called, it is given the parameters normally passed to an upgrade
2928operation (see the 'code' parameter documentation).
2929
2930=item * on_field (optional)
2931
2932If specified, this upgrade function is triggered upon the creation of
2933the field identified by this element. For instance,
2934
2935    on_field => 'MT::Foo->bar'
2936
2937This would specify that the upgrade step is only to run when the 'bar'
2938column is being added to the table that stores data for the MT::Foo
2939package.
2940
2941=item * code
2942
2943This coderef parameter is the declared handler for the upgrade
2944function. It is responsible for doing the upgrade task itself. For
2945quick operations, it is fine to do all of your work within this
2946subroutine. However, to faciliate large databases, it is important
2947to do that work in manageable portions so it doesn't time-out by
2948the web server or browser client.
2949
2950To facilitate an iterative process for your upgrade function, the
2951upgrade routine itself can yield a return value to signal the
2952upgrade process on how to proceed:
2953
2954=over 4
2955
2956=item * 0
2957
2958The upgrade function completed successfully.
2959
2960=item * undef
2961
2962upgrade routine failed with error. The error should be placed using the
2963MT::Upgrade-E<gt>error method.
2964
2965=item * E<gt> 0
2966
2967More work to do; the return value is the 'offset' parameter
2968to pass on the next invocation of the upgrade function.
2969
2970=back
2971
2972Due to the complexity of handling this kind of staged operation,
2973you will most likely want to use the prebuilt
2974'MT::Upgrade::core_update_records' routine to do most of your upgrade
2975operations that handle some or all records of a given package.
2976
2977If using the 'core_update_records' routine, you should also specify
2978an 'updater' parameter for your upgrade function.
2979
2980=item * updater
2981
2982This parameter is only used if you've specified the 'core_update_records'
2983routine (from the L<MT::Upgrade> package itself) for the 'code' element of
2984your upgrade function.
2985
2986    code => \&MT::Upgrade::core_update_records,
2987    updater => {
2988        class => 'MT::Foo',
2989        message => 'Updating Foo bars...',
2990        code => sub {
2991            my $foo = shift;
2992            $foo->bar(1);
2993        },
2994        condition => sub {
2995            my $foo = shift;
2996            !defined $foo->bar;
2997        },
2998        sql => 'update mt_foo set foo_bar = 1 where foo_bar is null'
2999    }
3000
3001This updater declaration is going to process all MT::Foo objects that
3002are available, setting the 'bar' property to 1 if it hasn't been assigned
3003a value already.
3004
3005Here's an overview of an 'updater' element:
3006
3007=over 4
3008
3009=item * class (required)
3010
3011The L<MT::Object>-descendant class to be processed.
3012
3013=item * code (required)
3014
3015A coderef to execute for B<each> record of the table. The parameter to
3016this routine is the object being processed. Following the call to your
3017subroutine, the object is saved for you, so you don't have to save
3018the object yourself.
3019
3020=item * message (optional)
3021
3022The status message to display when running this upgrade operation.
3023
3024=item * condition (optional)
3025
3026A coderef to use to test whether the current object needs to be upgraded
3027or not. This routine should return true if it is to be processed; false
3028if not. It is given the object as a parameter.
3029
3030=item * sql (optional)
3031
3032If specified, and if MT is using a SQL-based database for storing data,
3033this SQL statement is issued instead of doing the Perl-based row-by-row
3034upgrade.
3035
3036    sql => 'update mt_foo set foo_bar=1 where foo_bar is null'
3037
3038You may also specify multiple SQL statements using an array:
3039
3040    sql => [
3041        'update mt_foo set foo_bar=1 where foo_bar is null',
3042        'update mt_foo set foo_baz=2 where foo_baz is null'
3043    ]
3044
3045B<WARNING>: The 'sql' property is only meant to be used for cases where you
3046can issue simple, cross-database SQL statements. It is not advised to
3047use any vendor-specific SQL syntax. So, if you can't do that, don't specify
3048the 'sql' element at all and instead use the 'code' element exclusively
3049to do the upgrade operation.
3050
3051=back
3052
3053=back
3054
3055The declarative style of upgrade functions make it possible for MT to
3056fix itself, upgrading from any older schema version to the current one.
3057Upgrade functions are selected through an introspection process, so any
3058given upgrade operation may run a different selection of upgrade functions.
3059As such, it is important that any upgrade functions be written with this
3060in mind. Here are some general best practices to use when writing them:
3061
3062=over 4
3063
3064=item * Make them fast.
3065
3066Use the 'sql' element for a 'core_update_records' type upgrade function
3067so that SQL-based databases can be upgraded in one pass.
3068
3069=item * Make them indepedent.
3070
3071Don't assume that any other upgrade operation will have run within the
3072same application request. The upgrade process can run them in most any
3073order and across multiple application requests. You do have a guarantee
3074that a higher priority upgrade function will be run prior to a lower-priority
3075upgrade function (ie, assigning a priority of 1 will ensure it will run
3076before one with a priority of 2).
3077
3078=item * Limit them as much as possible.
3079
3080Specify a version_limit so it only runs for the proper schemas. Use the
3081condition element to bypass objects or the upgrade step altogether when
3082possible.
3083
3084=item * Repeating an upgrade function should be safe.
3085
3086This can be made possible through use of the 'condition' elements, bypassing
3087objects that have already been processed (see how the
3088'core_create_placements' upgrade function declares conditions for an
3089example).
3090
3091=item * Beware which translate method to call
3092
3093$self->translate_escape is for messages and phrases which appear on the
3094progress screen (therefore they are sent in JSON).  Use MT->translate
3095to messages and phrases which directly stored in the database.  Log messages
3096and objects' attributes fall into this category.
3097
3098=back
3099
3100=head1 AUTHOR & COPYRIGHTS
3101
3102Please see the I<MT> manpage for author, copyright, and license information.
3103
3104=cut
Note: See TracBrowser for help on using the browser.