root/branches/release-34/lib/MT/Upgrade.pm @ 1806

Revision 1806, 105.2 kB (checked in by fumiakiy, 20 months ago)

Pulled remove out of load_iter loop, and use update instead of save objects in load_iter loop to eliminate segfaults in SQLite. BugId:66602

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