root/branches/release-35/lib/MT/Upgrade.pm @ 1938

Revision 1938, 111.6 kB (checked in by bchoate, 20 months ago)

Added support for upgrading custom fields from meta column to narrow tables. BugId:69022

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