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

Revision 2056, 114.3 kB (checked in by bchoate, 19 months ago)

Removing delete meta use cache upgrade step since this was a meta field only used during development.

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