root/branches/release-29/lib/MT/Upgrade.pm @ 1333

Revision 1333, 102.0 kB (checked in by takayama, 22 months ago)

Fixed BugId:65812
* Changed scheme_version to 4.0037
* Added basename column to MT_Author

  • Assigning basename when author saved

* Changed to use MTAuthorBasename instead of MTAuthorDisplayName

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