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

Revision 2092, 114.4 kB (checked in by fumiakiy, 19 months ago)

Double check to see if the current schema has _meta column for the class in question.

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