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

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

Fix for upgrading meta for MT::Category when custom fields isn't installed. BugId:79376

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