root/branches/release-36/lib/MT/Upgrade.pm @ 2049

Revision 2049, 114.4 kB (checked in by auno, 19 months ago)

"cache_enable" valuable is removed from the page and cleaned up back-end codes. BugzID:79445

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