root/branches/release-38/lib/MT/Upgrade.pm @ 2305

Revision 2305, 118.2 kB (checked in by bchoate, 19 months ago)

Updates to fix SQLite meta upgrade bugs. BugId:79720

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