root/branches/release-39/lib/MT/CMS/Entry.pm @ 2548

Revision 2548, 79.3 kB (checked in by bchoate, 18 months ago)

Updates to iterator handling and use of 'window_size' argument for load_iter method of MT::Object. BugId:79247

  • Property svn:keywords set to Id Revision
Line 
1package MT::CMS::Entry;
2
3use strict;
4use MT::Util qw( format_ts relative_date remove_html encode_html encode_js
5    encode_url archive_file_for offset_time_list );
6use MT::I18N qw( substr_text const length_text wrap_text encode_text
7    break_up_text first_n_text guess_encoding );
8
9sub edit {
10    my $cb = shift;
11    my ($app, $id, $obj, $param) = @_;
12
13    my $q = $app->param;
14    my $type = $q->param('_type');
15    my $perms = $app->permissions;
16    my $blog_class = $app->model('blog');
17    my $blog = $app->blog;
18    my $blog_id = $blog->id;
19    my $author = $app->user;
20    my $class = $app->model($type);
21
22    # to trigger autosave logic in main edit routine
23    $param->{autosave_support} = 1;
24
25    if ($id) {
26        return $app->error( $app->translate("Invalid parameter") )
27          if $obj->class ne $type;
28
29        $param->{nav_entries} = 1;
30        $param->{entry_edit}  = 1;
31        if ( $type eq 'entry' ) {
32            $app->add_breadcrumb(
33                $app->translate('Entries'),
34                $app->uri(
35                    'mode' => 'list_entries',
36                    args   => { blog_id => $blog_id }
37                )
38            );
39        }
40        elsif ( $type eq 'page' ) {
41            $app->add_breadcrumb(
42                $app->translate('Pages'),
43                $app->uri(
44                    'mode' => 'list_pages',
45                    args   => { blog_id => $blog_id }
46                )
47            );
48        }
49        $app->add_breadcrumb( $obj->title
50              || $app->translate('(untitled)') );
51        ## Don't pass in author_id, because it will clash with the
52        ## author_id parameter of the author currently logged in.
53        delete $param->{'author_id'};
54        unless ( defined $q->param('category_id') ) {
55            delete $param->{'category_id'};
56            if ( my $cat = $obj->category ) {
57                $param->{category_id} = $cat->id;
58            }
59        }
60        $blog_id = $obj->blog_id;
61        my $blog = $app->model('blog')->load($blog_id);
62        my $status = $q->param('status') || $obj->status;
63        $param->{ "status_" . MT::Entry::status_text($status) } = 1;
64        $param->{ "allow_comments_"
65              . ( $q->param('allow_comments') || $obj->allow_comments || 0 )
66          } = 1;
67        $param->{'authored_on_date'} = $q->param('authored_on_date')
68          || format_ts( "%Y-%m-%d", $obj->authored_on, $blog, $app->user ? $app->user->preferred_language : undef );
69        $param->{'authored_on_time'} = $q->param('authored_on_time')
70          || format_ts( "%H:%M:%S", $obj->authored_on, $blog, $app->user ? $app->user->preferred_language : undef );
71
72        $param->{num_comments} = $id ? $obj->comment_count : 0;
73        $param->{num_pings} = $id ? $obj->ping_count : 0;
74
75        # Check permission to send notifications and if the
76        # blog has notification list subscribers
77        if (   $perms->can_send_notifications
78            && $obj->status == MT::Entry::RELEASE() )
79        {
80            my $not_class = $app->model('notification');
81            $param->{can_send_notifications} = 1;
82            $param->{has_subscribers} =
83              $not_class->exist( { blog_id => $blog_id } );
84        }
85
86        ## Load next and previous entries for next/previous links
87        if ( my $next = $obj->next ) {
88            $param->{next_entry_id} = $next->id;
89        }
90        if ( my $prev = $obj->previous ) {
91            $param->{previous_entry_id} = $prev->id;
92        }
93
94        $param->{has_any_pinged_urls} = ( $obj->pinged_urls || '' ) =~ m/\S/;
95        $param->{ping_errors}         = $q->param('ping_errors');
96        $param->{can_view_log}        = $app->user->can_view_log;
97        $param->{entry_permalink}     = $obj->permalink;
98        $param->{'mode_view_entry'}   = 1;
99        $param->{'basename_old'}      = $obj->basename;
100
101        if ( my $ts = $obj->authored_on ) {
102            $param->{authored_on_ts} = $ts;
103            $param->{authored_on_formatted} =
104              format_ts( MT::App::CMS::LISTING_DATETIME_FORMAT(), $ts, $blog, $app->user ? $app->user->preferred_language : undef );
105        }
106
107        $app->load_list_actions( $type, $param );
108    } else {
109        $param->{entry_edit} = 1;
110        if ($blog_id) {
111            if ( $type eq 'entry' ) {
112                $app->add_breadcrumb(
113                    $app->translate('Entries'),
114                    $app->uri(
115                        'mode' => 'list_entries',
116                        args   => { blog_id => $blog_id }
117                    )
118                );
119                $app->add_breadcrumb( $app->translate('New Entry') );
120                $param->{nav_new_entry} = 1;
121            }
122            elsif ( $type eq 'page' ) {
123                $app->add_breadcrumb(
124                    $app->translate('Pages'),
125                    $app->uri(
126                        'mode' => 'list_pages',
127                        args   => { blog_id => $blog_id }
128                    )
129                );
130                $app->add_breadcrumb( $app->translate('New Page') );
131                $param->{nav_new_page} = 1;
132            }
133        }
134
135        # (if there is no blog_id parameter, this is a
136        # bookmarklet post and doesn't need breadcrumbs.)
137        delete $param->{'author_id'};
138        delete $param->{'pinged_urls'};
139        my $blog_timezone = 0;
140        if ($blog_id) {
141            my $blog = $blog_class->load($blog_id)
142                or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
143            $blog_timezone = $blog->server_offset();
144
145            # new entry defaults used for new entries AND new pages.
146            my $def_status = $q->param('status')
147              || $blog->status_default;
148            if ($def_status) {
149                $param->{ "status_"
150                      . MT::Entry::status_text($def_status) } = 1;
151            }
152            if ( $param->{status} ) {
153                $param->{ 'allow_comments_'
154                      . $q->param('allow_comments') } = 1;
155                $param->{allow_comments} = $q->param('allow_comments');
156                $param->{allow_pings}    = $q->param('allow_pings');
157            }
158            else {
159                # new edit
160                $param->{ 'allow_comments_'
161                      . $blog->allow_comments_default } = 1;
162                $param->{allow_comments} = $blog->allow_comments_default;
163                $param->{allow_pings}    = $blog->allow_pings_default;
164            }
165        }
166
167        require POSIX;
168        my @now = offset_time_list( time, $blog );
169        $param->{authored_on_date} = $q->param('authored_on_date')
170          || POSIX::strftime( "%Y-%m-%d", @now );
171        $param->{authored_on_time} = $q->param('authored_on_time')
172          || POSIX::strftime( "%H:%M:%S", @now );
173    }
174
175    ## Load categories and process into loop for category pull-down.
176    require MT::Placement;
177    my $cat_id = $param->{category_id};
178    my $depth  = 0;
179    my %places;
180
181    # set the dirty flag in js?
182    $param->{dirty} = $q->param('dirty') ? 1 : 0;
183
184    if ($id) {
185        my @places =
186          MT::Placement->load( { entry_id => $id, is_primary => 0 } );
187        %places = map { $_->category_id => 1 } @places;
188    }
189    my $cats = $q->param('category_ids');
190    if ( defined $cats ) {
191        if ( my @cats = split /,/, $cats ) {
192            $cat_id = $cats[0];
193            %places = map { $_ => 1 } @cats;
194        }
195    }
196    if ( $q->param('reedit') ) {
197        $param->{reedit} = 1;
198        if ( !$q->param('basename_manual') ) {
199            $param->{'basename'} = '';
200        }
201    }
202    if ($blog) {
203        $param->{file_extension} = $blog->file_extension || '';
204        $param->{file_extension} = '.' . $param->{file_extension}
205          if $param->{file_extension} ne '';
206    }
207    else {
208        $param->{file_extension} = 'html';
209    }
210
211    ## Now load user's preferences and customization for new/edit
212    ## entry page.
213    if ($perms) {
214        my $pref_param = $app->load_entry_prefs( $perms->entry_prefs );
215        %$param = ( %$param, %$pref_param );
216        $param->{disp_prefs_bar_colspan} = $param->{new_object} ? 1 : 2;
217
218        # Completion for tags
219        my $auth_prefs = $author->entry_prefs;
220        if ( my $delim = chr( $auth_prefs->{tag_delim} ) ) {
221            if ( $delim eq ',' ) {
222                $param->{'auth_pref_tag_delim_comma'} = 1;
223            }
224            elsif ( $delim eq ' ' ) {
225                $param->{'auth_pref_tag_delim_space'} = 1;
226            }
227            else {
228                $param->{'auth_pref_tag_delim_other'} = 1;
229            }
230            $param->{'auth_pref_tag_delim'} = $delim;
231        }
232
233        require JSON;
234        my $json = JSON->new( autoconv => 0 ); # stringifies numbers this way
235        $param->{tags_js} =
236          $json->objToJson(
237            MT::Tag->cache( blog_id => $blog_id, class => 'MT::Entry', private => 1 )
238          );
239
240        $param->{can_edit_categories} = $perms->can_edit_categories;
241    }
242
243    my $data = $app->_build_category_list(
244        blog_id => $blog_id,
245        markers => 1,
246        type    => $class->container_type,
247    );
248    my $top_cat = $cat_id;
249    my @sel_cats;
250    my $cat_tree = [];
251    if ( $type eq 'page' ) {
252        push @$cat_tree,
253          {
254            id    => -1,
255            label => '/',
256            basename => '/',
257            path  => [],
258          };
259        $top_cat ||= -1;
260    }
261    foreach (@$data) {
262        next unless exists $_->{category_id};
263        if ( $type eq 'page' ) {
264            $_->{category_path_ids} ||= [];
265            unshift @{ $_->{category_path_ids} }, -1;
266        }
267        push @$cat_tree,
268          {
269            id => $_->{category_id},
270            label => $_->{category_label} . ( $type eq 'page' ? '/' : '' ),
271            basename => $_->{category_basename} . ( $type eq 'page' ? '/' : '' ),
272            path => $_->{category_path_ids} || [],
273          };
274        push @sel_cats, $_->{category_id}
275          if $places{ $_->{category_id} }
276          && $_->{category_id} != $cat_id;
277    }
278    $param->{category_tree} = $cat_tree;
279    unshift @sel_cats, $top_cat if defined $top_cat && $top_cat ne "";
280    $param->{selected_category_loop}   = \@sel_cats;
281    $param->{have_multiple_categories} = scalar @$data > 1;
282
283    $param->{basename_limit} = ( $blog ? $blog->basename_limit : 0 ) || 30;
284
285    if ( $q->param('tags') ) {
286        $param->{tags} = $q->param('tags');
287    }
288    else {
289        if ($obj) {
290            my $tag_delim = chr( $app->user->entry_prefs->{tag_delim} );
291            require MT::Tag;
292            my $tags = MT::Tag->join( $tag_delim, $obj->tags );
293            $param->{tags} = $tags;
294        }
295    }
296
297    ## Load text filters if user displays them
298    my %entry_filters;
299    if ( defined( my $filter = $q->param('convert_breaks') ) ) {
300        $entry_filters{$filter} = 1;
301    }
302    elsif ($obj) {
303        %entry_filters = map { $_ => 1 } @{ $obj->text_filters };
304    }
305    elsif ($blog) {
306        my $cb = $author->text_format || $blog->convert_paras;
307        $cb = '__default__' if $cb eq '1';
308        $entry_filters{$cb} = 1;
309        $param->{convert_breaks} = $cb;
310    }
311    my $filters = MT->all_text_filters;
312    $param->{text_filters} = [];
313    for my $filter ( keys %$filters ) {
314        push @{ $param->{text_filters} },
315          {
316            filter_key      => $filter,
317            filter_label    => $filters->{$filter}{label},
318            filter_selected => $entry_filters{$filter},
319            filter_docs     => $filters->{$filter}{docs},
320          };
321    }
322    $param->{text_filters} =
323      [ sort { $a->{filter_key} cmp $b->{filter_key} }
324          @{ $param->{text_filters} } ];
325    unshift @{ $param->{text_filters} },
326      {
327        filter_key      => '0',
328        filter_label    => $app->translate('None'),
329        filter_selected => ( !keys %entry_filters ),
330      };
331
332    if ($blog) {
333        if ( !defined $param->{convert_breaks} ) {
334            my $cb = $blog->convert_paras;
335            $cb = '__default__' if $cb eq '1';
336            $param->{convert_breaks} = $cb;
337        }
338        my $ext = ( $blog->file_extension || '' );
339        $ext = '.' . $ext if $ext ne '';
340        $param->{blog_file_extension} = $ext;
341    }
342
343    my $rte;
344    if ($param->{convert_breaks} eq 'richtext') {
345        ## Rich Text editor
346        $rte = lc($app->config('RichTextEditor'));
347    }
348    else {
349        $rte = 'archetype';
350    }
351    my $editors = $app->registry("richtext_editors");
352    my $edit_reg = $editors->{$rte} || $editors->{archetype};
353    my $rich_editor_tmpl;
354    if ($rich_editor_tmpl = $edit_reg->{plugin}->load_tmpl($edit_reg->{template})) {
355        $param->{rich_editor} = $rte;
356        $param->{rich_editor_tmpl} = $rich_editor_tmpl;
357    }
358
359    $param->{object_type}  = $type;
360    $param->{quickpost_js} = MT::CMS::Entry::quickpost_js($app, $type);
361    if ( 'page' eq $type ) {
362        $param->{search_label} = $app->translate('pages');
363        $param->{output}       = 'edit_entry.tmpl';
364        $param->{screen_class} = 'edit-page edit-entry';
365    }
366    $param->{sitepath_configured} = $blog && $blog->site_path ? 1 : 0;
367    1;
368}
369
370sub list {
371    my $app = shift;
372    my ($param) = @_;
373    $param ||= {};
374
375    require MT::Entry;
376    my $type = $param->{type} || MT::Entry->class_type;
377    my $pkg = $app->model($type) or return "Invalid request.";
378
379    my $q     = $app->param;
380    my $perms = $app->permissions;
381    if ( $type eq 'page' ) {
382        if ( $perms
383            && ( !$perms->can_manage_pages ) )
384        {
385            return $app->errtrans("Permission denied.");
386        }
387    }
388    else {
389        if (
390            $perms
391            && (   !$perms->can_edit_all_posts
392                && !$perms->can_create_post
393                && !$perms->can_publish_post )
394          )
395        {
396            return $app->errtrans("Permission denied.");
397        }
398    }
399
400    my $list_pref = $app->list_pref($type);
401    my %param     = %$list_pref;
402    my $blog_id   = $q->param('blog_id');
403    my %terms;
404    $terms{blog_id} = $blog_id if $blog_id;
405    $terms{class} = $type;
406    my $limit = $list_pref->{rows};
407    my $offset = $app->param('offset') || 0;
408
409    if ( !$blog_id && !$app->user->is_superuser ) {
410        require MT::Permission;
411        $terms{blog_id} = [
412            map { $_->blog_id }
413              grep { $_->can_create_post || $_->can_edit_all_posts }
414              MT::Permission->load( { author_id => $app->user->id } )
415        ];
416    }
417
418    my %arg;
419    my $filter_key = $q->param('filter_key') || '';
420    my $filter_col = $q->param('filter')     || '';
421    my $filter_val = $q->param('filter_val');
422
423    # check blog_id for deciding to apply category filter or not
424    my ( $filter_name, $filter_value );    # human-readable versions
425    if ( !exists( $terms{$filter_col} ) ) {
426        if ( $filter_col eq 'category_id' ) {
427            $filter_name = $app->translate('Category');
428            require MT::Category;
429            my $cat = MT::Category->load($filter_val);
430            return $app->errtrans( "Load failed: [_1]", MT::Category->errstr )
431              unless $cat;
432            if ( $cat->blog_id != $blog_id ) {
433                $filter_key = '';
434                $filter_col = '';
435                $filter_val = '';
436            }
437            $filter_value = $cat->label;
438        }
439        elsif ( $filter_col eq 'asset_id' ) {
440            $filter_name = $app->translate('Asset');
441            my $asset_class = MT->model('asset');
442            my $asset = $asset_class->load($filter_val);
443            return $app->errtrans( "Load failed: [_1]", $asset_class->errstr )
444              unless $asset;
445            if ($asset->blog_id != $blog_id) {
446                $filter_key = '';
447                $filter_col = '';
448                $filter_val = '';
449            }
450            $filter_value = $param{asset_label} = $asset->label;
451        }
452    }
453    if ( $filter_col && $filter_val ) {
454        if ( 'power_edit' eq $filter_col ) {
455            $filter_col = 'id';
456            unless ( 'ARRAY' eq ref($filter_val) ) {
457                my @values = $app->param('filter_val');
458                $filter_val = \@values;
459            }
460        }
461        if ( !exists( $terms{$filter_col} ) ) {
462            if ( $filter_col eq 'category_id' ) {
463                $arg{'join'} = MT::Placement->join_on(
464                    'entry_id',
465                    { category_id => $filter_val },
466                    { unique      => 1 }
467                );
468            }
469            elsif ( $filter_col eq 'asset_id' ) {
470                $arg{'join'} = MT->model('objectasset')->join_on(
471                    'object_id',
472                    { object_ds => $type,
473                      asset_id  => $filter_val },
474                    { unique    => 1 }
475                );
476            }
477            elsif (( $filter_col eq 'normalizedtag' )
478                || ( $filter_col eq 'exacttag' ) )
479            {
480                my $normalize = ( $filter_col eq 'normalizedtag' );
481                require MT::Tag;
482                require MT::ObjectTag;
483                my $tag_delim   = chr( $app->user->entry_prefs->{tag_delim} );
484                my @filter_vals = MT::Tag->split( $tag_delim, $filter_val );
485                my @filter_tags = @filter_vals;
486                if ($normalize) {
487                    push @filter_tags, MT::Tag->normalize($_)
488                      foreach @filter_vals;
489                }
490                my @tags = MT::Tag->load( { name => [@filter_tags] },
491                    { binary => { name => 1 } } );
492                my @tag_ids;
493                foreach (@tags) {
494                    push @tag_ids, $_->id;
495                    if ($normalize) {
496                        my @more = MT::Tag->load(
497                            { n8d_id => $_->n8d_id ? $_->n8d_id : $_->id } );
498                        push @tag_ids, $_->id foreach @more;
499                    }
500                }
501                @tag_ids = (0) unless @tags;
502                $arg{'join'} = MT::ObjectTag->join_on(
503                    'object_id',
504                    {
505                        tag_id            => \@tag_ids,
506                        object_datasource => $pkg->datasource
507                    },
508                    { unique => 1 }
509                );
510            }
511            else {
512                $terms{$filter_col} = $filter_val;
513            }
514            $param{filter_args} = "&filter=" . encode_url($filter_col) . "&filter_val=" . encode_url($filter_val);
515
516            if (   ( $filter_col eq 'normalizedtag' )
517                || ( $filter_col eq 'exacttag' ) )
518            {
519                $filter_name  = $app->translate('Tag');
520                $filter_value = $filter_val;
521            }
522            elsif ( $filter_col eq 'author_id' ) {
523                $filter_name = $app->translate('User');
524                my $author = MT::Author->load($filter_val);
525                return $app->errtrans( "Load failed: [_1]", MT::Author->errstr )
526                  unless $author;
527                $filter_value = $author->name;
528            }
529            elsif ( $filter_col eq 'status' ) {
530                $filter_name = $app->translate('Entry Status');
531                $filter_value =
532                  $app->translate( MT::Entry::status_text($filter_val) );
533            }
534            if ( $filter_name && $filter_value ) {
535                $param{filter}                        = $filter_col;
536                $param{ 'filter_col_' . $filter_col } = 1;
537                $param{filter_val}                    = $filter_val;
538            }
539        }
540        $param{filter_unpub} = $filter_col eq 'status';
541    }
542    elsif ($filter_key) {
543        my $filters = $app->registry("list_filters", "entry") || {};
544        if ( my $filter = $filters->{$filter_key} ) {
545            if ( my $code = $filter->{code}
546                || $app->handler_to_coderef( $filter->{handler} ) )
547            {
548                $param{filter_key}   = $filter_key;
549                $param{filter_label} = $filter->{label};
550                $code->( \%terms, \%arg );
551            }
552        }
553    }
554    require MT::Category;
555    require MT::Placement;
556
557    my $total = $pkg->count( \%terms, \%arg ) || 0;
558    $arg{'sort'} = $type eq 'page' ? 'modified_on' : 'authored_on';
559    $arg{direction} = 'descend';
560    $arg{limit}     = $limit + 1;
561    if ( $total <= $limit ) {
562        delete $arg{limit};
563        $offset = 0;
564    }
565    elsif ( $total && $offset > $total - 1 ) {
566        $arg{offset} = $offset = $total - $limit;
567    }
568    elsif ( $offset && ( ( $offset < 0 ) || ( $total - $offset < $limit ) ) ) {
569        $arg{offset} = $offset = $total - $limit;
570    }
571    else {
572        $arg{offset} = $offset if $offset;
573    }
574
575    my $iter = $pkg->load_iter( \%terms, \%arg );
576
577    my $is_power_edit = $q->param('is_power_edit');
578    if ($is_power_edit) {
579        $param{has_expanded_mode} = 0;
580        delete $param{view_expanded};
581    }
582    else {
583        $param{has_expanded_mode} = 1;
584    }
585    my $data = build_entry_table( $app,
586        iter          => $iter,
587        is_power_edit => $is_power_edit,
588        param         => \%param,
589        type          => $type
590    );
591    delete $_->{object} foreach @$data;
592    delete $param{entry_table} unless @$data;
593
594    ## We tried to load $limit + 1 entries above; if we actually got
595    ## $limit + 1 back, we know we have another page of entries.
596    my $have_next_entry = @$data > $limit;
597    pop @$data while @$data > $limit;
598    if ($offset) {
599        $param{prev_offset}     = 1;
600        $param{prev_offset_val} = $offset - $limit;
601        $param{prev_offset_val} = 0 if $param{prev_offset_val} < 0;
602    }
603    if ($have_next_entry) {
604        $param{next_offset}     = 1;
605        $param{next_offset_val} = $offset + $limit;
606    }
607
608    $iter = MT::Author->load_iter(
609        { type => MT::Author::AUTHOR() },
610        {
611            'join' => $pkg->join_on(
612                'author_id',
613                { $blog_id ? ( blog_id => $blog_id ) : () },
614                {
615                    'unique'  => 1,
616                    'sort'    => 'authored_on',
617                    direction => 'descend'
618                }
619            ),
620            limit => 51,
621        }
622    );
623    my %seen;
624    my @authors;
625    while ( my $au = $iter->() ) {
626        next if $seen{ $au->id };
627        $seen{ $au->id } = 1;
628        my $row = {
629            author_name => $au->name,
630            author_id   => $au->id
631        };
632        push @authors, $row;
633        if ( @authors == 50 ) {
634            $iter->end;
635            last;
636        }
637    }
638    $param{entry_author_loop} = \@authors;
639
640    # $iter = $app->model('asset')->load_iter(
641    #     { class => '*', blog_id => $blog_id },
642    #     {
643    #         'join' => MT->model('objectasset')->join_on(
644    #             'asset_id',
645    #             {},
646    #             { unique => 1 }
647    #         ),
648    #         'sort'    => 'created_on',
649    #         direction => 'descend',
650    #     }
651    # );
652    # %seen = ();
653    # my @assets;
654    # while ( my $asset = $iter->() ) {
655    #     next if $seen{ $asset->id };
656    #     $seen{ $asset->id } = 1;
657    #     my $row = {
658    #         asset_label => $asset->label,
659    #         asset_id    => $asset->id,
660    #     };
661    #     push @assets, $row;
662    #     if ( @assets == 50 ) {
663    #         $iter->end;
664    #         last;
665    #     }
666    # }
667    # if ($filter_col eq 'asset_id' && !$seen{$filter_val}) {
668    #     push @assets, {
669    #         asset_label => $param{asset_label},
670    #         asset_id    => $filter_val,
671    #     };
672    # }
673    # $param{entry_asset_loop} = \@assets;
674
675    $param{page_actions}        = $app->page_actions( $app->mode );
676    $param{list_filters}        = $app->list_filters('entry');
677    $param{can_power_edit}      = $blog_id && !$is_power_edit;
678    $param{can_republish}       = $blog_id ? $perms->can_rebuild : 1;
679    $param{is_power_edit}       = $is_power_edit;
680    $param{saved_deleted}       = $q->param('saved_deleted');
681    $param{no_rebuild}          = $q->param('no_rebuild');
682    $param{saved}               = $q->param('saved');
683    $param{limit}               = $limit;
684    $param{offset}              = $offset;
685    $param{object_type}         = $type;
686    $param{object_label}        = $pkg->class_label;
687    $param{object_label_plural} = $param{search_label} =
688      $pkg->class_label_plural;
689    $param{list_start}  = $offset + 1;
690    $param{list_end}    = $offset + scalar @$data;
691    $param{list_total}  = $total;
692    $param{next_max}    = $param{list_total} - $limit;
693    $param{next_max}    = 0 if ( $param{next_max} || 0 ) < $offset + 1;
694    $param{nav_entries} = 1;
695    $param{feed_label}  = $app->translate( "[_1] Feed", $pkg->class_label );
696    $param{feed_url} =
697      $app->make_feed_link( $type, $blog_id ? { blog_id => $blog_id } : undef );
698    $app->add_breadcrumb( $pkg->class_label_plural );
699    $param{listing_screen} = 1;
700
701    unless ($blog_id) {
702        $param{system_overview_nav} = 1;
703    }
704    $param{container_label} = $pkg->container_label;
705    unless ( $param{screen_class} ) {
706        $param{screen_class} = "list-$type";
707        $param{screen_class} .= " list-entry"
708          if $param{object_type} eq "page";  # to piggyback on list-entry styles
709    }
710    $param{mode}            = $app->mode;
711    if ( my $blog = MT::Blog->load($blog_id) ) {
712        $param{sitepath_unconfigured} = $blog->site_path ? 0 : 1;
713    }
714
715    $param->{return_args} ||= $app->make_return_args;
716    my @return_args = grep { $_ !~ /offset=\d/ } split /&/, $param->{return_args};
717    $param{return_args} = join '&', @return_args;
718    $param{return_args} .= "&offset=$offset" if $offset;
719    $param{screen_id} = "list-entry";
720    $param{screen_id} = "list-page"
721      if $param{object_type} eq "page";
722    if ( $param{is_power_edit} ) {
723        $param{screen_id} = "batch-edit-entry";
724        $param{screen_id} = "batch-edit-page"
725          if $param{object_type} eq "page";
726        $param{screen_class} .= " batch-edit";
727    }
728    $app->load_tmpl( "list_entry.tmpl", \%param );
729}
730
731sub preview {
732    my $app         = shift;
733    my $q           = $app->param;
734    my $type        = $q->param('_type') || 'entry';
735    my $entry_class = $app->model($type);
736    my $blog_id     = $q->param('blog_id');
737    my $blog        = $app->blog;
738    my $id          = $q->param('id');
739    my $entry;
740    my $user_id = $app->user->id;
741
742    if ($id) {
743        $entry = $entry_class->load( { id => $id, blog_id => $blog_id } )
744            or return $app->errtrans( "Invalid request." );
745        $user_id = $entry->author_id;
746    }
747    else {
748        $entry = $entry_class->new;
749        $entry->author_id($user_id);
750        $entry->id(-1); # fake out things like MT::Taggable::__load_tags
751        $entry->blog_id($blog_id);
752    }
753    my $cat;
754    my $names = $entry->column_names;
755
756    my %values = map { $_ => scalar $app->param($_) } @$names;
757    delete $values{'id'} unless $q->param('id');
758    ## Strip linefeed characters.
759    for my $col (qw( text excerpt text_more keywords )) {
760        $values{$col} =~ tr/\r//d if $values{$col};
761    }
762    $values{allow_comments} = 0
763      if !defined( $values{allow_comments} )
764      || $q->param('allow_comments') eq '';
765    $values{allow_pings} = 0
766      if !defined( $values{allow_pings} )
767      || $q->param('allow_pings') eq '';
768    $entry->set_values( \%values );
769
770    my $cat_ids = $q->param('category_ids');
771    if ($cat_ids) {
772        my @cats = split /,/, $cat_ids;
773        if (@cats) {
774            my $primary_cat = $cats[0];
775            $cat =
776              MT::Category->load( { id => $primary_cat, blog_id => $blog_id } );
777            my @categories = MT::Category->load( { id => \@cats, blog_id => $blog_id });
778            $entry->cache_property('category', undef, $cat);
779            $entry->cache_property('categories', undef, \@categories);
780        }
781    } else {
782        $entry->cache_property('category', undef, undef);
783        $entry->cache_property('categories', undef, []);
784    }
785    my $tag_delim = chr( $app->user->entry_prefs->{tag_delim} );
786    my @tag_names = MT::Tag->split( $tag_delim, $q->param('tags') );
787    if (@tag_names) {
788        my @tags;
789        foreach my $tag_name (@tag_names) {
790            my $tag = MT::Tag->new;
791            $tag->name($tag_name);
792            push @tags, $tag;
793        }
794        $entry->{__tags} = \@tag_names;
795        $entry->{__tag_objects} = \@tags;
796    }
797
798    my $date = $q->param('authored_on_date');
799    my $time = $q->param('authored_on_time');
800    my $ts   = $date . $time;
801    $ts =~ s/\D//g;
802    $entry->authored_on($ts);
803
804    my $preview_basename = $app->preview_object_basename;
805    $entry->basename($preview_basename);
806
807    require MT::TemplateMap;
808    require MT::Template;
809    my $tmpl_map = MT::TemplateMap->load(
810        {
811            archive_type => ( $type eq 'page' ? 'Page' : 'Individual' ),
812            is_preferred => 1,
813            blog_id => $blog_id,
814        }
815    );
816
817    my $tmpl;
818    my $fullscreen;
819    my $archive_file;
820    my $orig_file;
821    my $file_ext;
822    if ($tmpl_map) {
823        $tmpl         = MT::Template->load( $tmpl_map->template_id );
824        $file_ext = $blog->file_extension || '';
825        $archive_file = $entry->archive_file;
826
827        my $blog_path = $type eq 'page' ?
828            $blog->site_path :
829            ($blog->archive_path || $blog->site_path);
830        $archive_file = File::Spec->catfile( $blog_path, $archive_file );
831        require File::Basename;
832        my $path;
833        ( $orig_file, $path ) = File::Basename::fileparse( $archive_file );
834        $file_ext = '.' . $file_ext if $file_ext ne '';
835        $archive_file = File::Spec->catfile( $path, $preview_basename . $file_ext );
836    }
837    else {
838        $tmpl       = $app->load_tmpl('preview_entry_content.tmpl');
839        $fullscreen = 1;
840    }
841    return $app->error($app->translate('Can\'t load template.')) unless $tmpl;
842
843    # translates naughty words when PublishCharset is NOT UTF-8
844    $app->_translate_naughty_words($entry);
845
846    $entry->convert_breaks( scalar $q->param('convert_breaks') );
847       
848    my @data = ( { data_name => 'author_id', data_value => $user_id } );
849    $app->run_callbacks( 'cms_pre_preview', $app, $entry, \@data );
850
851    my $ctx = $tmpl->context;
852    $ctx->stash( 'entry', $entry );
853    $ctx->stash( 'blog',  $blog );
854    $ctx->stash( 'category', $cat ) if $cat;
855    $ctx->{current_timestamp} = $ts;
856    $ctx->var('entry_template',    1);
857    $ctx->var('archive_template',  1);
858    $ctx->var('entry_template',    1);
859    $ctx->var('feedback_template', 1);
860    $ctx->var('archive_class',     'entry-archive');
861    $ctx->var('preview_template',  1);
862    my $html = $tmpl->output;
863    my %param;
864    unless ( defined($html) ) {
865        my $preview_error = $app->translate( "Publish error: [_1]",
866            MT::Util::encode_html( $tmpl->errstr ) );
867        $param{preview_error} = $preview_error;
868        my $tmpl_plain = $app->load_tmpl('preview_entry_content.tmpl');
869        $tmpl->text( $tmpl_plain->text );
870        $html = $tmpl->output;
871        defined($html)
872          or return $app->error(
873            $app->translate( "Publish error: [_1]", $tmpl->errstr ) );
874        $fullscreen = 1;
875    }
876
877    # If MT is configured to do 'local' previews, convert all
878    # the normal blog URLs into the domain used by MT itself (ie,
879    # blog is published to www.example.com, which is a different
880    # server from where MT runs, mt.example.com; previews therefore
881    # should occur locally, so replace all http://www.example.com/
882    # with http://mt.example.com/).
883    my ($old_url, $new_url);
884    if ($app->config('LocalPreviews')) {
885        $old_url = $blog->site_url;
886        $old_url =~ s!^(https?://[^/]+?/)(.*)?!$1!;
887        $new_url = $app->base . '/';
888        $html =~ s!\Q$old_url\E!$new_url!g;
889    }
890
891    if ( !$fullscreen ) {
892        my $fmgr = $blog->file_mgr;
893
894        ## Determine if we need to build directory structure,
895        ## and build it if we do. DirUmask determines
896        ## directory permissions.
897        require File::Basename;
898        my $path = File::Basename::dirname($archive_file);
899        $path =~ s!/$!!
900          unless $path eq '/';    ## OS X doesn't like / at the end in mkdir().
901        unless ( $fmgr->exists($path) ) {
902            $fmgr->mkpath($path);
903        }
904
905        if ( $fmgr->exists($path) && $fmgr->can_write($path) ) {
906            $fmgr->put_data( $html, $archive_file );
907            $param{preview_file} = $preview_basename;
908            my $preview_url = $entry->archive_url;
909            $preview_url =~ s! / \Q$orig_file\E ( /? ) $!/$preview_basename$file_ext$1!x;
910
911            # We also have to translate the URL used for the
912            # published file to be on the MT app domain.
913            if (defined $new_url) {
914                $preview_url =~ s!^\Q$old_url\E!$new_url!;
915            }
916
917            $param{preview_url}  = $preview_url;
918
919            # we have to make a record of this preview just in case it
920            # isn't cleaned up by re-editing, saving or cancelling on
921            # by the user.
922            require MT::Session;
923            my $sess_obj = MT::Session->get_by_key(
924                {
925                    id   => $preview_basename,
926                    kind => 'TF',                # TF = Temporary File
927                    name => $archive_file,
928                }
929            );
930            $sess_obj->start(time);
931            $sess_obj->save;
932        }
933        else {
934            $fullscreen = 1;
935            $param{preview_error} = $app->translate(
936                "Unable to create preview file in this location: [_1]", $path );
937            my $tmpl_plain = $app->load_tmpl('preview_entry_content.tmpl');
938            $tmpl->text( $tmpl_plain->text );
939            $tmpl->reset_tokens;
940            $html = $tmpl->output;
941            $param{preview_body} = $html;
942        }
943    }
944    else {
945        $param{preview_body} = $html;
946    }
947    $param{id} = $id if $id;
948    $param{new_object} = $param{id} ? 0 : 1;
949    $param{title} = $entry->title;
950    my $cols = $entry_class->column_names;
951
952    for my $col (@$cols) {
953        next
954          if $col eq 'created_on'
955          || $col eq 'created_by'
956          || $col eq 'modified_on'
957          || $col eq 'modified_by'
958          || $col eq 'authored_on'
959          || $col eq 'author_id'
960          || $col eq 'pinged_urls'
961          || $col eq 'tangent_cache'
962          || $col eq 'template_id'
963          || $col eq 'class'
964          || $col eq 'meta'
965          || $col eq 'comment_count'
966          || $col eq 'ping_count';
967        if ( $col eq 'basename' ) {
968            if (   ( !defined $q->param('basename') )
969                || ( $q->param('basename') eq '' ) )
970            {
971                $q->param( 'basename', $q->param('basename_old') );
972            }
973        }
974        push @data,
975          {
976            data_name  => $col,
977            data_value => scalar $q->param($col)
978          };
979    }
980    for my $data (
981        qw( authored_on_date authored_on_time basename_manual basename_old category_ids tags )
982      )
983    {
984        push @data,
985          {
986            data_name  => $data,
987            data_value => scalar $q->param($data)
988          };
989    }
990
991    $param{entry_loop} = \@data;
992    my $list_mode;
993    my $list_title;
994    if ( $type eq 'page' ) {
995        $list_title = 'Pages';
996        $list_mode  = 'list_pages';
997    }
998    else {
999        $list_title = 'Entries';
1000        $list_mode  = 'list_entries';
1001    }
1002    if ($id) {
1003        $app->add_breadcrumb(
1004            $app->translate($list_title),
1005            $app->uri(
1006                'mode' => $list_mode,
1007                args   => { blog_id => $blog_id }
1008            )
1009        );
1010        $app->add_breadcrumb( $entry->title || $app->translate('(untitled)') );
1011    }
1012    else {
1013        $app->add_breadcrumb( $app->translate($list_title),
1014            $app->uri( 'mode' => $list_mode, args => { blog_id => $blog_id } )
1015        );
1016        $app->add_breadcrumb(
1017            $app->translate( 'New [_1]', $entry_class->class_label ) );
1018        $param{nav_new_entry} = 1;
1019    }
1020    $param{object_type}  = $type;
1021    $param{object_label} = $entry_class->class_label;
1022    if ($fullscreen) {
1023        return $app->load_tmpl( 'preview_entry.tmpl', \%param );
1024    }
1025    else {
1026        return $app->load_tmpl( 'preview_strip.tmpl', \%param );
1027    }
1028}
1029
1030sub cfg_entry {
1031    my $app     = shift;
1032    my $q       = $app->param;
1033    my $blog_id = scalar $q->param('blog_id');
1034    return $app->return_to_dashboard( redirect => 1 )
1035      unless $blog_id;
1036    $q->param( '_type', 'blog' );
1037    $q->param( 'id',    scalar $q->param('blog_id') );
1038    $app->forward("view",
1039        {
1040            output       => 'cfg_entry.tmpl',
1041            screen_class => 'settings-screen entry-screen'
1042        }
1043    );
1044}
1045
1046sub save {
1047    my $app = shift;
1048    $app->validate_magic or return;
1049
1050    $app->remove_preview_file;
1051
1052    if ( $app->param('is_power_edit') ) {
1053        return $app->save_entries(@_);
1054    }
1055    my $author = $app->user;
1056    my $type = $app->param('_type') || 'entry';
1057
1058    my $class = $app->model($type)
1059      or retrun $app->errtrans("Invalid parameter");
1060
1061    my $cat_class = $app->model( $class->container_type );
1062
1063    my $perms = $app->permissions
1064      or return $app->errtrans("Permission denied.");
1065
1066    if ( $type eq 'page' ) {
1067        return $app->errtrans("Permission denied.")
1068          unless $perms->can_manage_pages;
1069    }
1070
1071    my $id = $app->param('id');
1072    if ( !$id ) {
1073        return $app->errtrans("Permission denied.")
1074          unless ( ( 'entry' eq $type ) && $perms->can_create_post )
1075          || ( ( 'page' eq $type ) && $perms->can_manage_pages );
1076    }
1077
1078    $app->validate_magic() or return;
1079
1080    # check for autosave
1081    if ( $app->param('_autosave') ) {
1082        return $app->autosave_object();
1083    }
1084
1085    require MT::Blog;
1086    my $blog_id = $app->param('blog_id');
1087    my $blog    = MT::Blog->load($blog_id)
1088        or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
1089   
1090    my $archive_type;
1091
1092    my ( $obj, $orig_obj, $orig_file );
1093    if ($id) {
1094        $obj = $class->load($id)
1095          || return $app->error(
1096            $app->translate( "No such [_1].", $class->class_label ) );
1097        return $app->error( $app->translate("Invalid parameter") )
1098          unless $obj->blog_id == $blog_id;
1099        if ( $type eq 'entry' ) {
1100            return $app->error( $app->translate("Permission denied.") )
1101              unless $perms->can_edit_entry( $obj, $author );
1102            return $app->error( $app->translate("Permission denied.") )
1103              if ( $obj->status ne $app->param('status') )
1104              && !( $perms->can_edit_entry( $obj, $author, 1 ) );
1105            $archive_type = 'Individual';
1106        }
1107        elsif ( $type eq 'page' ) {
1108            $archive_type = 'Page';
1109        }
1110        $orig_obj = $obj->clone;
1111        $orig_file = archive_file_for( $orig_obj, $blog, $archive_type );
1112    }
1113    else {
1114        $obj = $class->new;
1115    }
1116    my $status_old = $id ? $obj->status : 0;
1117    my $names = $obj->column_names;
1118
1119    ## Get rid of category_id param, because we don't want to just set it
1120    ## in the Entry record; save it for later when we will set the Placement.
1121    my ( $cat_id, @add_cat ) = split /\s*,\s*/,
1122      ( $app->param('category_ids') || '' );
1123    $app->delete_param('category_id');
1124    if ($id) {
1125        ## Delete the author_id param (if present), because we don't want to
1126        ## change the existing author.
1127        $app->delete_param('author_id');
1128    }
1129
1130    my %values = map { $_ => scalar $app->param($_) } @$names;
1131    delete $values{'id'} unless $app->param('id');
1132    ## Strip linefeed characters.
1133    for my $col (qw( text excerpt text_more keywords )) {
1134        $values{$col} =~ tr/\r//d if $values{$col};
1135    }
1136    $values{allow_comments} = 0
1137      if !defined( $values{allow_comments} )
1138      || $app->param('allow_comments') eq '';
1139    delete $values{week_number}
1140      if ( $app->param('week_number') || '' ) eq '';
1141    delete $values{basename}
1142      unless $perms->can_publish_post || $perms->can_edit_all_posts;
1143    $obj->set_values( \%values );
1144    $obj->allow_pings(0)
1145      if !defined $app->param('allow_pings')
1146      || $app->param('allow_pings') eq '';
1147    my $ao_d = $app->param('authored_on_date');
1148    my $ao_t = $app->param('authored_on_time');
1149
1150    if ( !$id ) {
1151
1152        #  basename check for this new entry...
1153        if (   ( my $basename = $app->param('basename') )
1154            && !$app->param('basename_manual')
1155            && $type eq 'entry' )
1156        {
1157            my $exist =
1158              $class->exist( { blog_id => $blog_id, basename => $basename } );
1159            if ($exist) {
1160                $obj->basename( MT::Util::make_unique_basename($obj) );
1161            }
1162        }
1163    }
1164
1165    if ( $type eq 'page' ) {
1166
1167        # -1 is a special id for identifying the 'root' folder
1168        $cat_id = 0 if $cat_id == -1;
1169        my $dup_it = $class->load_iter(
1170            {
1171                blog_id  => $blog_id,
1172                basename => $obj->basename,
1173                class    => 'page',
1174                ( $id ? ( id => $id ) : () )
1175            },
1176            { ( $id ? ( not => { id => 1 } ) : () ) }
1177        );
1178        while ( my $p = $dup_it->() ) {
1179            my $p_folder = $p->folder;
1180            my $dup_folder_path =
1181              defined $p_folder ? $p_folder->publish_path() : '';
1182            my $folder = MT::Folder->load($cat_id) if $cat_id;
1183            my $folder_path = defined $folder ? $folder->publish_path() : '';
1184            return $app->error(
1185                $app->translate(
1186"Same Basename has already been used. You should use an unique basename."
1187                )
1188            ) if ( $dup_folder_path eq $folder_path );
1189        }
1190
1191    }
1192
1193    if ( $type eq 'entry' ) {
1194        $obj->status( MT::Entry::HOLD() )
1195          if !$id
1196          && !$perms->can_publish_post
1197          && !$perms->can_edit_all_posts;
1198    }
1199
1200    my $filter_result = $app->run_callbacks( 'cms_save_filter.' . $type, $app );
1201
1202    if ( !$filter_result ) {
1203        my %param = ();
1204        $param{error}       = $app->errstr;
1205        $param{return_args} = $app->param('return_args');
1206        return $app->forward( "view", \%param );
1207    }
1208
1209    # check to make sure blog has site url and path defined.
1210    # otherwise, we can't publish a released entry
1211    if ( ( $obj->status || 0 ) != MT::Entry::HOLD() ) {
1212        if ( !$blog->site_path || !$blog->site_url ) {
1213            return $app->error(
1214                $app->translate(
1215"Your blog has not been configured with a site path and URL. You cannot publish entries until these are defined."
1216                )
1217            );
1218        }
1219    }
1220
1221    my ( $previous_old, $next_old );
1222    if (   ( $perms->can_publish_post || $perms->can_edit_all_posts )
1223        && ($ao_d) )
1224    {
1225        my %param = ();
1226        my $ao = $ao_d . ' ' . $ao_t;
1227        unless (
1228            $ao =~ m!^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$! )
1229        {
1230            $param{error} = $app->translate(
1231"Invalid date '[_1]'; authored on dates must be in the format YYYY-MM-DD HH:MM:SS.",
1232$ao
1233            );
1234        }
1235        my $s = $6 || 0;
1236            $param{error} = $app->translate(
1237                "Invalid date '[_1]'; authored on dates should be real dates.",
1238                $ao
1239          )
1240          if (
1241               $s > 59
1242            || $s < 0
1243            || $5 > 59
1244            || $5 < 0
1245            || $4 > 23
1246            || $4 < 0
1247            || $2 > 12
1248            || $2 < 1
1249            || $3 < 1
1250            || ( MT::Util::days_in( $2, $1 ) < $3
1251                && !MT::Util::leap_day( $0, $1, $2 ) )
1252          );
1253        $param{return_args} = $app->param('return_args');
1254        return $app->forward( "view", \%param ) if $param{error};
1255        if ($obj->authored_on) {
1256            $previous_old = $obj->previous(1);
1257            $next_old     = $obj->next(1);
1258        }
1259        my $ts = sprintf "%04d%02d%02d%02d%02d%02d", $1, $2, $3, $4, $5, $s;
1260        $obj->authored_on($ts);
1261    }
1262    my $is_new = $obj->id ? 0 : 1;
1263
1264    $app->_translate_naughty_words($obj);
1265
1266    $obj->modified_by( $author->id ) unless $is_new;
1267
1268    $app->run_callbacks( 'cms_pre_save.' . $type, $app, $obj, $orig_obj )
1269      || return $app->error(
1270        $app->translate(
1271            "Saving [_1] failed: [_2]",
1272            $class->class_label, $app->errstr
1273        )
1274      );
1275
1276    $obj->save
1277      or return $app->error(
1278        $app->translate(
1279            "Saving [_1] failed: [_2]",
1280            $class->class_label, $obj->errstr
1281        )
1282      );
1283
1284    my $message;
1285    if ($is_new) {
1286        $message =
1287          $app->translate( "[_1] '[_2]' (ID:[_3]) added by user '[_4]'",
1288            $class->class_label, $obj->title, $obj->id, $author->name );
1289    }
1290    elsif ( $orig_obj->status ne $obj->status ) {
1291        $message = $app->translate(
1292"[_1] '[_2]' (ID:[_3]) edited and its status changed from [_4] to [_5] by user '[_6]'",
1293            $class->class_label,
1294            $obj->title,
1295            $obj->id,
1296            $app->translate( MT::Entry::status_text( $orig_obj->status ) ),
1297            $app->translate( MT::Entry::status_text( $obj->status ) ),
1298            $author->name
1299        );
1300
1301    }
1302    else {
1303        $message =
1304          $app->translate( "[_1] '[_2]' (ID:[_3]) edited by user '[_4]'",
1305            $class->class_label, $obj->title, $obj->id, $author->name );
1306    }
1307    require MT::Log;
1308    $app->log(
1309        {
1310            message => $message,
1311            level   => MT::Log::INFO(),
1312            class   => $type,
1313            $is_new ? ( category => 'new' ) : ( category => 'edit' ),
1314            metadata => $obj->id
1315        }
1316    );
1317
1318    my $error_string = MT::callback_errstr();
1319
1320    ## Now that the object is saved, we can be certain that it has an
1321    ## ID. So we can now add/update/remove the primary placement.
1322    require MT::Placement;
1323    my $place =
1324      MT::Placement->load( { entry_id => $obj->id, is_primary => 1 } );
1325    if ($cat_id) {
1326        unless ($place) {
1327            $place = MT::Placement->new;
1328            $place->entry_id( $obj->id );
1329            $place->blog_id( $obj->blog_id );
1330            $place->is_primary(1);
1331        }
1332        $place->category_id($cat_id);
1333        $place->save;
1334        my $cat = $cat_class->load($cat_id);
1335        $obj->cache_property( 'category', undef, $cat );
1336    }
1337    else {
1338        if ( $place ) {
1339            $place->remove;
1340            $obj->cache_property( 'category', undef, undef );
1341        }
1342    }
1343
1344    my $placements_updated;
1345
1346    # save secondary placements...
1347    my @place = MT::Placement->load(
1348        {
1349            entry_id   => $obj->id,
1350            is_primary => 0
1351        }
1352    );
1353    for my $place (@place) {
1354        $place->remove;
1355        $placements_updated = 1;
1356    }
1357    my @add_cat_obj;
1358    for my $cat_id (@add_cat) {
1359        my $cat = $cat_class->load($cat_id);
1360
1361        # blog_id sanity check
1362        next if !$cat || $cat->blog_id != $obj->blog_id;
1363
1364        my $place = MT::Placement->new;
1365        $place->entry_id( $obj->id );
1366        $place->blog_id( $obj->blog_id );
1367        $place->is_primary(0);
1368        $place->category_id($cat_id);
1369        $place->save
1370          or return $app->error(
1371            $app->translate( "Saving placement failed: [_1]", $place->errstr )
1372          );
1373        $placements_updated = 1;
1374        push @add_cat_obj, $cat;
1375    }
1376    if ($placements_updated) {
1377        unshift @add_cat_obj, $obj->cache_property('category')
1378          if $obj->cache_property('category');
1379        $obj->cache_property( 'categories', undef, [] );
1380        $obj->cache_property( 'categories', undef, \@add_cat_obj );
1381    }
1382
1383    $app->run_callbacks( 'cms_post_save.' . $type, $app, $obj, $orig_obj );
1384
1385    ## If the saved status is RELEASE, or if the *previous* status was
1386    ## RELEASE, then rebuild entry archives, indexes, and send the
1387    ## XML-RPC ping(s). Otherwise the status was and is HOLD, and we
1388    ## don't have to do anything.
1389    if ( ( $obj->status || 0 ) == MT::Entry::RELEASE()
1390        || $status_old eq MT::Entry::RELEASE() )
1391    {
1392        if ( $app->config('DeleteFilesAtRebuild') && $orig_obj ) {
1393            my $file = archive_file_for( $obj, $blog, $archive_type );
1394            if ( $file ne $orig_file || $obj->status != MT::Entry::RELEASE() ) {
1395                $app->publisher->remove_entry_archive_file(
1396                    Entry       => $orig_obj,
1397                    ArchiveType => $archive_type
1398                );
1399            }
1400        }
1401
1402        # If there are no static pages, just rebuild indexes.
1403        if ( $blog->count_static_templates($archive_type) == 0
1404            || MT::Util->launch_background_tasks() )
1405        {
1406            my $res = MT::Util::start_background_task(
1407                sub {
1408                    $app->run_callbacks('pre_build');
1409                    $app->rebuild_entry(
1410                        Entry             => $obj,
1411                        BuildDependencies => 1,
1412                        OldEntry          => $orig_obj,
1413                        OldPrevious       => ($previous_old)
1414                        ? $previous_old->id
1415                        : undef,
1416                        OldNext => ($next_old) ? $next_old->id : undef
1417                    ) or return $app->publish_error();
1418                    $app->run_callbacks( 'rebuild', $blog );
1419                    $app->run_callbacks( 'post_build' );
1420                    1;
1421                }
1422            );
1423            return unless $res;
1424            return ping_continuation($app,
1425                $obj, $blog,
1426                OldStatus => $status_old,
1427                IsNew     => $is_new,
1428            );
1429        }
1430        else {
1431            return $app->redirect(
1432                $app->uri(
1433                    'mode' => 'start_rebuild',
1434                    args   => {
1435                        blog_id    => $obj->blog_id,
1436                        'next'     => 0,
1437                        type       => 'entry-' . $obj->id,
1438                        entry_id   => $obj->id,
1439                        is_new     => $is_new,
1440                        old_status => $status_old,
1441                        (
1442                            $previous_old
1443                            ? ( old_previous => $previous_old->id )
1444                            : ()
1445                        ),
1446                        ( $next_old ? ( old_next => $next_old->id ) : () )
1447                    }
1448                )
1449            );
1450        }
1451    }
1452    _finish_rebuild_ping( $app, $obj, !$id );
1453}
1454
1455sub save_entries {
1456    my $app   = shift;
1457    my $perms = $app->permissions;
1458    my $type  = $app->param('_type');
1459    return $app->errtrans("Permission denied.")
1460      unless $perms
1461      && (
1462        $type eq 'page'
1463        ? ( $perms->can_manage_pages )
1464        : (      $perms->can_publish_post
1465              || $perms->can_create_post
1466              || $perms->can_edit_all_posts )
1467      );
1468
1469    $app->validate_magic() or return;
1470
1471    my $q = $app->param;
1472    my @p = $q->param;
1473    require MT::Entry;
1474    require MT::Placement;
1475    require MT::Log;
1476    my $blog_id        = $q->param('blog_id');
1477    my $this_author    = $app->user;
1478    my $this_author_id = $this_author->id;
1479    for my $p (@p) {
1480        next unless $p =~ /^category_id_(\d+)/;
1481        my $id    = $1;
1482        my $entry = MT::Entry->load($id)
1483            or next;
1484        return $app->error( $app->translate("Permission denied.") )
1485            unless $perms
1486              && (
1487                $type eq 'page'
1488                ? ( $perms->can_manage_pages )
1489                : (      $perms->can_publish_post
1490                      || $perms->can_create_post
1491                      || $perms->can_edit_all_posts )
1492              );
1493        my $orig_obj = $entry->clone;
1494        if ( $perms->can_edit_entry( $entry, $this_author ) ) {
1495            my $author_id = $q->param( 'author_id_' . $id );
1496            $entry->author_id( $author_id ? $author_id : 0 );
1497            $entry->title( scalar $q->param( 'title_' . $id ) );
1498        }
1499        if ( $perms->can_edit_entry( $entry, $this_author, 1 ) )
1500        {    ## can he/she change status?
1501            $entry->status( scalar $q->param( 'status_' . $id ) );
1502            my $co = $q->param( 'created_on_' . $id );
1503            unless ( $co =~
1504                m!(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})(?::(\d{2}))?! )
1505            {
1506                return $app->error(
1507                    $app->translate(
1508"Invalid date '[_1]'; authored on dates must be in the format YYYY-MM-DD HH:MM:SS.",
1509                        $co
1510                    )
1511                );
1512            }
1513            my $s = $6 || 0;
1514
1515            # Emit an error message if the date is bogus.
1516            return $app->error(
1517                $app->translate(
1518"Invalid date '[_1]'; authored on dates should be real dates.",
1519                    $co
1520                )
1521              )
1522              if $s > 59
1523              || $s < 0
1524              || $5 > 59
1525              || $5 < 0
1526              || $4 > 23
1527              || $4 < 0
1528              || $2 > 12
1529              || $2 < 1
1530              || $3 < 1
1531              || ( MT::Util::days_in( $2, $1 ) < $3
1532                && !MT::Util::leap_day( $0, $1, $2 ) );
1533
1534            # FIXME: Should be assigning the publish_date field here
1535            my $ts = sprintf "%04d%02d%02d%02d%02d%02d", $1, $2, $3, $4, $5, $s;
1536            if ($type eq 'page' ) {
1537                $entry->modified_on($ts);
1538            } else {
1539                $entry->authored_on($ts);
1540            }
1541        }
1542        $app->run_callbacks( 'cms_pre_save.' . $type, $app, $entry, $orig_obj )
1543          || return $app->error(
1544            $app->translate(
1545                "Saving [_1] failed: [_2]",
1546                $entry->class_label, $app->errstr
1547            )
1548          );
1549        $entry->save
1550          or return $app->error(
1551            $app->translate(
1552                "Saving entry '[_1]' failed: [_2]", $entry->title,
1553                $entry->errstr
1554            )
1555          );
1556        my $cat_id = $q->param("category_id_$id");
1557        my $place  = MT::Placement->load(
1558            {
1559                entry_id   => $id,
1560                is_primary => 1
1561            }
1562        );
1563        if ( $place && !$cat_id ) {
1564            $place->remove
1565              or return $app->error(
1566                $app->translate(
1567                    "Removing placement failed: [_1]",
1568                    $place->errstr
1569                )
1570              );
1571        }
1572        elsif ($cat_id) {
1573            unless ($place) {
1574                $place = MT::Placement->new;
1575                $place->entry_id($id);
1576                $place->blog_id($blog_id);
1577                $place->is_primary(1);
1578            }
1579            $place->category_id( scalar $q->param($p) );
1580            $place->save
1581              or return $app->error(
1582                $app->translate(
1583                    "Saving placement failed: [_1]",
1584                    $place->errstr
1585                )
1586              );
1587        }
1588        my $message;
1589        if ( $orig_obj->status ne $entry->status ) {
1590            $message = $app->translate(
1591"[_1] '[_2]' (ID:[_3]) edited and its status changed from [_4] to [_5] by user '[_6]'",
1592                $entry->class_label,
1593                $entry->title,
1594                $entry->id,
1595                $app->translate( MT::Entry::status_text( $orig_obj->status ) ),
1596                $app->translate( MT::Entry::status_text( $entry->status ) ),
1597                $this_author->name
1598            );
1599        }
1600        else {
1601            $message =
1602              $app->translate( "[_1] '[_2]' (ID:[_3]) edited by user '[_4]'",
1603                $entry->class_label, $entry->title, $entry->id,
1604                $this_author->name );
1605        }
1606        $app->log(
1607            {
1608                message  => $message,
1609                level    => MT::Log::INFO(),
1610                class    => $entry->class,
1611                category => 'edit',
1612                metadata => $entry->id
1613            }
1614        );
1615        $app->run_callbacks( 'cms_post_save.' . $type, $app, $entry, $orig_obj );
1616    }
1617    $app->add_return_arg( 'saved' => 1, is_power_edit => 1 );
1618    $app->call_return;
1619}
1620
1621sub send_pings {
1622    my $app = shift;
1623    my $q   = $app->param;
1624    $app->validate_magic() or return;
1625    require MT::Entry;
1626    require MT::Blog;
1627    my $blog  = MT::Blog->load( scalar $q->param('blog_id') );
1628    my $entry = MT::Entry->load( scalar $q->param('entry_id') );
1629    ## MT::ping_and_save pings each of the necessary URLs, then processes
1630    ## the return value from MT::ping to update the list of URLs pinged
1631    ## and not successfully pinged. It returns the return value from
1632    ## MT::ping for further processing. If a fatal error occurs, it returns
1633    ## undef.
1634    my $results = $app->ping_and_save(
1635        Blog      => $blog,
1636        Entry     => $entry,
1637        OldStatus => scalar $q->param('old_status')
1638    ) or return;
1639    my $has_errors = 0;
1640    require MT::Log;
1641    for my $res (@$results) {
1642        $has_errors++,
1643          $app->log(
1644            {
1645                message => $app->translate(
1646                    "Ping '[_1]' failed: [_2]",
1647                    $res->{url},
1648                    encode_text( $res->{error}, undef, undef )
1649                ),
1650                class => 'system',
1651                level => MT::Log::WARNING()
1652            }
1653          ) unless $res->{good};
1654    }
1655    _finish_rebuild_ping( $app, $entry, scalar $q->param('is_new'),
1656        $has_errors );
1657}
1658
1659sub pinged_urls {
1660    my $app   = shift;
1661    my $perms = $app->permissions
1662      or return $app->error( $app->translate("No permissions") );
1663    my %param;
1664    my $entry_id = $app->param('entry_id');
1665    require MT::Entry;
1666    my $entry = MT::Entry->load($entry_id)
1667        or return $app->error($app->translate('Can\'t load entry #[_1].', $entry_id));
1668    $param{url_loop} = [ map { { url => $_ } } @{ $entry->pinged_url_list } ];
1669    $param{failed_url_loop} =
1670      [ map { { url => $_ } }
1671          @{ $entry->pinged_url_list( OnlyFailures => 1 ) } ];
1672    $app->load_tmpl( 'popup/pinged_urls.tmpl', \%param );
1673}
1674
1675sub save_entry_prefs {
1676    my $app   = shift;
1677    my $perms = $app->permissions
1678      or return $app->error( $app->translate("No permissions") );
1679    $app->validate_magic() or return;
1680    my $q     = $app->param;
1681    my $prefs = $app->_entry_prefs_from_params;
1682    $perms->entry_prefs($prefs);
1683    $perms->save
1684      or return $app->error(
1685        $app->translate( "Saving permissions failed: [_1]", $perms->errstr ) );
1686    $app->send_http_header("text/json");
1687    return "true";
1688}
1689
1690sub publish_entries {
1691    my $app = shift;
1692    require MT::Entry;
1693    update_entry_status( $app, MT::Entry::RELEASE(), $app->param('id') );
1694}
1695
1696sub draft_entries {
1697    my $app = shift;
1698    require MT::Entry;
1699    update_entry_status( $app, MT::Entry::HOLD(), $app->param('id') );
1700}
1701
1702sub open_batch_editor {
1703    my $app = shift;
1704    my @ids = $app->param('id');
1705
1706    $app->param( 'is_power_edit', 1 );
1707    $app->param( 'filter',        'power_edit' );
1708    $app->param( 'filter_val',    \@ids );
1709    $app->mode(
1710        'list_' . ( 'entry' eq $app->param('_type') ? 'entries' : 'pages' ) );
1711    $app->forward( "list_entry", { type => $app->param('_type') } );
1712}
1713
1714sub build_entry_table {
1715    my $app = shift;
1716    my (%args) = @_;
1717
1718    my $app_author = $app->user;
1719    my $perms      = $app->permissions;
1720    my $type       = $args{type};
1721    my $class      = $app->model($type);
1722
1723    my $list_pref = $app->list_pref($type);
1724    if ( $args{is_power_edit} ) {
1725        delete $list_pref->{view_expanded};
1726    }
1727    my $iter;
1728    if ( $args{load_args} ) {
1729        $iter = $class->load_iter( @{ $args{load_args} } );
1730    }
1731    elsif ( $args{iter} ) {
1732        $iter = $args{iter};
1733    }
1734    elsif ( $args{items} ) {
1735        $iter = sub { shift @{ $args{items} } };
1736    }
1737    return [] unless $iter;
1738    my $limit         = $args{limit};
1739    my $is_power_edit = $args{is_power_edit} || 0;
1740    my $param         = $args{param} || {};
1741
1742    ## Load list of categories for display in filter pulldown (and selection
1743    ## pulldown on power edit page).
1744    my ( $c_data, %cats );
1745    my $blog_id = $app->param('blog_id');
1746    if ($blog_id) {
1747        $c_data = $app->_build_category_list(
1748            blog_id => $blog_id,
1749            type    => $class->container_type,
1750        );
1751        my $i = 0;
1752        for my $row (@$c_data) {
1753            $row->{category_index} = $i++;
1754            my $spacer = $row->{category_label_spacer} || '';
1755            $spacer =~ s/\&nbsp;/\\u00A0/g;
1756            $row->{category_label_js} =
1757              $spacer . encode_js( $row->{category_label} );
1758            $cats{ $row->{category_id} } = $row;
1759        }
1760        $param->{category_loop} = $c_data;
1761    }
1762
1763    my ( $date_format, $datetime_format );
1764
1765    if ($is_power_edit) {
1766        $date_format          = "%Y.%m.%d";
1767        $datetime_format      = "%Y-%m-%d %H:%M:%S";
1768    }
1769    else {
1770        $date_format     = MT::App::CMS::LISTING_DATE_FORMAT();
1771        $datetime_format = MT::App::CMS::LISTING_DATETIME_FORMAT();
1772    }
1773
1774    my @cat_list;
1775    if ($is_power_edit) {
1776        @cat_list =
1777          sort { $cats{$a}->{category_index} <=> $cats{$b}->{category_index} }
1778          keys %cats;
1779    }
1780
1781    my @data;
1782    my %blogs;
1783    require MT::Blog;
1784    my $title_max_len   = const('DISPLAY_LENGTH_EDIT_ENTRY_TITLE');
1785    my $excerpt_max_len = const('DISPLAY_LENGTH_EDIT_ENTRY_TEXT_FROM_EXCERPT');
1786    my $text_max_len    = const('DISPLAY_LENGTH_EDIT_ENTRY_TEXT_BREAK_UP');
1787    my %blog_perms;
1788    $blog_perms{ $perms->blog_id } = $perms if $perms;
1789    while ( my $obj = $iter->() ) {
1790        my $blog_perms;
1791        if ( !$app_author->is_superuser() ) {
1792            $blog_perms = $blog_perms{ $obj->blog_id }
1793              || $app_author->blog_perm( $obj->blog_id );
1794        }
1795
1796        my $row = $obj->column_values;
1797        $row->{text} ||= '';
1798        if ( my $ts =
1799            ( $type eq 'page' ) ? $obj->modified_on : $obj->authored_on )
1800        {
1801            $row->{created_on_formatted} =
1802              format_ts( $date_format, $ts, $obj->blog, $app->user ? $app->user->preferred_language : undef );
1803            $row->{created_on_time_formatted} =
1804              format_ts( $datetime_format, $ts, $obj->blog, $app->user ? $app->user->preferred_language : undef );
1805            $row->{created_on_relative} =
1806              relative_date( $ts, time, $obj->blog );
1807        }
1808        my $author = $obj->author;
1809        $row->{author_name} =
1810          $author ? $author->name : $app->translate('(user deleted)');
1811        if ( my $cat = $obj->category ) {
1812            $row->{category_label}    = $cat->label;
1813            $row->{category_basename} = $cat->basename;
1814        }
1815        else {
1816            $row->{category_label}    = '';
1817            $row->{category_basename} = '';
1818        }
1819        $row->{file_extension} = $obj->blog ? $obj->blog->file_extension : '';
1820        $row->{title_short} = $obj->title;
1821        if ( !defined( $row->{title_short} ) || $row->{title_short} eq '' ) {
1822            my $title = remove_html( $obj->text );
1823            $row->{title_short} =
1824              substr_text( defined($title) ? $title : "", 0, $title_max_len )
1825              . '...';
1826        }
1827        else {
1828            $row->{title_short} = remove_html( $row->{title_short} );
1829            $row->{title_short} =
1830              substr_text( $row->{title_short}, 0, $title_max_len + 3 ) . '...'
1831              if length_text( $row->{title_short} ) > $title_max_len;
1832        }
1833        if ( $row->{excerpt} ) {
1834            $row->{excerpt} = remove_html( $row->{excerpt} );
1835        }
1836        if ( !$row->{excerpt} ) {
1837            my $text = remove_html( $row->{text} ) || '';
1838            $row->{excerpt} = first_n_text( $text, $excerpt_max_len );
1839            if ( length($text) > length( $row->{excerpt} ) ) {
1840                $row->{excerpt} .= ' ...';
1841            }
1842        }
1843        $row->{text} = break_up_text( $row->{text}, $text_max_len )
1844          if $row->{text};
1845        $row->{title_long} = remove_html( $obj->title );
1846        $row->{status_text} =
1847          $app->translate( MT::Entry::status_text( $obj->status ) );
1848        $row->{ "status_" . MT::Entry::status_text( $obj->status ) } = 1;
1849        $row->{has_edit_access} = $app_author->is_superuser
1850          || ( ( 'entry' eq $type )
1851            && $blog_perms
1852            && $blog_perms->can_edit_entry( $obj, $app_author ) )
1853          || ( ( 'page' eq $type )
1854            && $blog_perms
1855            && $blog_perms->can_manage_pages );
1856        if ($is_power_edit) {
1857            $row->{has_publish_access} = $app_author->is_superuser
1858              || ( ( 'entry' eq $type )
1859                && $blog_perms
1860                && $blog_perms->can_edit_entry( $obj, $app_author, 1 ) )
1861              || ( ( 'page' eq $type )
1862                && $blog_perms
1863                && $blog_perms->can_manage_pages );
1864            $row->{is_editable} = $row->{has_edit_access};
1865
1866            ## This is annoying. In order to generate and pre-select the
1867            ## category, user, and status pull down menus, we need to
1868            ## have a separate *copy* of the list of categories and
1869            ## users for every entry listed, so that each row in the list
1870            ## can "know" whether it is selected for this entry or not.
1871            my @this_c_data;
1872            my $this_category_id = $obj->category ? $obj->category->id : undef;
1873            for my $c_id (@cat_list) {
1874                push @this_c_data, { %{ $cats{$c_id} } };
1875                $this_c_data[-1]{category_is_selected} = $this_category_id
1876                  && $this_category_id == $c_id ? 1 : 0;
1877            }
1878            $row->{row_category_loop} = \@this_c_data;
1879
1880            if ( $obj->author ) {
1881                $row->{row_author_name} = $obj->author->name;
1882                $row->{row_author_id}   = $obj->author->id;
1883            } else {
1884                $row->{row_author_name} = $app->translate(
1885                    '(user deleted - ID:[_1])',
1886                    $obj->author_id
1887                );
1888                $row->{row_author_id} = $obj->author_id,
1889             }
1890        }
1891        if ( my $blog = $blogs{ $obj->blog_id } ||=
1892            MT::Blog->load( $obj->blog_id ) )
1893        {
1894            $row->{weblog_id}   = $blog->id;
1895            $row->{weblog_name} = $blog->name;
1896        }
1897        if ( $obj->status == MT::Entry::RELEASE() ) {
1898            $row->{entry_permalink} = $obj->permalink;
1899        }
1900        $row->{object} = $obj;
1901        push @data, $row;
1902    }
1903    return [] unless @data;
1904
1905    $param->{entry_table}[0] = {%$list_pref};
1906    $param->{object_loop} = $param->{entry_table}[0]{object_loop} = \@data;
1907    $app->load_list_actions( $type, \%$param )
1908      unless $is_power_edit;
1909    \@data;
1910}
1911
1912sub quickpost_js {
1913    my $app     = shift;
1914    my ($type)  = @_;
1915    my $blog_id = $app->blog->id;
1916    my $blog    = $app->model('blog')->load($blog_id)
1917        or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
1918    my %args    = ( '_type' => $type, blog_id => $blog_id, qp => 1 );
1919    my $uri = $app->base . $app->uri( 'mode' => 'view', args => \%args );
1920    my $script = qq!javascript:d=document;w=window;t='';if(d.selection)t=d.selection.createRange().text;else{if(d.getSelection)t=d.getSelection();else{if(w.getSelection)t=w.getSelection()}}void(w.open('$uri&title='+encodeURIComponent(d.title)+'&text='+encodeURIComponent(d.location.href)+encodeURIComponent('<br/><br/>')+encodeURIComponent(t),'_blank','scrollbars=yes,status=yes,resizable=yes,location=yes'))!;
1921    # Translate the phrase here to avoid ActivePerl DLL bug.
1922    $app->translate('<a href="[_1]">QuickPost to [_2]</a> - Drag this link to your browser\'s toolbar then click it when you are on a site you want to blog about.', encode_html($script), $blog->name);
1923}
1924
1925sub can_view {
1926    my ( $eh, $app, $id, $objp ) = @_;
1927    my $perms = $app->permissions;
1928    if (   !$id
1929        && !$perms->can_create_post )
1930    {
1931        return 0;
1932    }
1933    if ($id) {
1934        my $obj = $objp->force();
1935        if ( !$perms->can_edit_entry( $obj, $app->user ) ) {
1936            return 0;
1937        }
1938    }
1939    1;
1940}
1941
1942sub can_delete {
1943    my ( $eh, $app, $obj ) = @_;
1944    my $author = $app->user;
1945    return 1 if $author->is_superuser();
1946    my $perms = $app->permissions;
1947    if ( !$perms || $perms->blog_id != $obj->blog_id ) {
1948        $perms ||= $author->permissions( $obj->blog_id );
1949    }
1950    return $perms && $perms->can_edit_entry( $obj, $author );
1951}
1952
1953sub pre_save {
1954    my $eh = shift;
1955    my ( $app, $obj ) = @_;
1956
1957    # save tags
1958    my $tags = $app->param('tags');
1959    if ( defined $tags ) {
1960        my $blog = $app->blog;
1961        my $fields = $blog->smart_replace_fields;
1962        if ( $fields =~ m/tags/ig ) {
1963            $tags = MT::App::CMS::_convert_word_chars( $app, $tags );
1964        }
1965
1966        require MT::Tag;
1967        my $tag_delim = chr( $app->user->entry_prefs->{tag_delim} );
1968        my @tags = MT::Tag->split( $tag_delim, $tags );
1969        if (@tags) {
1970            $obj->set_tags(@tags);
1971        }
1972        else {
1973            $obj->remove_tags();
1974        }
1975    }
1976
1977    # update text heights if necessary
1978    if ( my $perms = $app->permissions ) {
1979        my $prefs = $perms->entry_prefs || $app->load_default_entry_prefs;
1980        my $text_height = $app->param('text_height');
1981        if ( defined $text_height ) {
1982            my ($pref_text_height) = $prefs =~ m/\bbody:(\d+)\b/;
1983            $pref_text_height ||= 0;
1984            if ( $text_height != $pref_text_height ) {
1985                if ( $prefs =~ m/\bbody\b/ ) {
1986                    $prefs =~ s/\bbody(:\d+)\b/body:$text_height/;
1987                }
1988                else {
1989                    $prefs = 'body:' . $text_height . ',' . $prefs;
1990                }
1991            }
1992        }
1993        if ( $prefs ne ( $perms->entry_prefs || '' ) ) {
1994            $perms->entry_prefs($prefs);
1995            $perms->save;
1996        }
1997    }
1998    $obj->discover_tb_from_entry();
1999    1;
2000}
2001
2002sub post_save {
2003    my $eh = shift;
2004    my ( $app, $obj ) = @_;
2005    my $sess_obj = $app->autosave_session_obj;
2006    $sess_obj->remove if $sess_obj;
2007    1;
2008}
2009
2010sub post_delete {
2011    my ( $eh, $app, $obj ) = @_;
2012
2013    my $sess_obj = $app->autosave_session_obj;
2014    $sess_obj->remove if $sess_obj;
2015
2016    $app->log(
2017        {
2018            message => $app->translate(
2019                "Entry '[_1]' (ID:[_2]) deleted by '[_3]'",
2020                $obj->title, $obj->id, $app->user->name
2021            ),
2022            level    => MT::Log::INFO(),
2023            class    => 'system',
2024            category => 'delete'
2025        }
2026    );
2027}
2028
2029sub update_entry_status {
2030    my $app = shift;
2031    my ( $new_status, @ids ) = @_;
2032    return $app->errtrans("Need a status to update entries")
2033      unless $new_status;
2034    return $app->errtrans("Need entries to update status")
2035      unless @ids;
2036    my @bad_ids;
2037    my %rebuild_these;
2038    require MT::Entry;
2039
2040    foreach my $id (@ids) {
2041        my $entry = MT::Entry->load($id)
2042          or return $app->errtrans(
2043            "One of the entries ([_1]) did not actually exist", $id );
2044        next if $entry->status == $new_status;
2045        if ( $app->config('DeleteFilesAtRebuild')
2046            && ( MT::Entry::RELEASE() eq $entry->status ) )
2047        {
2048            my $archive_type =
2049              $entry->class eq 'page'
2050              ? 'Page'
2051              : 'Individual';
2052            $app->publisher->remove_entry_archive_file(
2053                Entry       => $entry,
2054                ArchiveType => $archive_type
2055            );
2056        }
2057        my $old_status = $entry->status;
2058        $entry->status($new_status);
2059        $entry->save() and $rebuild_these{$id} = 1;
2060        my $message = $app->translate(
2061            "[_1] '[_2]' (ID:[_3]) status changed from [_4] to [_5]",
2062            $entry->class_label,
2063            $entry->title,
2064            $entry->id,
2065            $app->translate( MT::Entry::status_text($old_status) ),
2066            $app->translate( MT::Entry::status_text($new_status) )
2067        );
2068        $app->log(
2069            {
2070                message  => $message,
2071                level    => MT::Log::INFO(),
2072                class    => $entry->class,
2073                category => 'edit',
2074                metadata => $entry->id
2075            }
2076        );
2077    }
2078    $app->rebuild_these( \%rebuild_these, how => MT::App::CMS::NEW_PHASE() );
2079}
2080
2081sub _finish_rebuild_ping {
2082    my $app = shift;
2083    my ( $entry, $is_new, $ping_errors ) = @_;
2084    $app->redirect(
2085        $app->uri(
2086            'mode' => 'view',
2087            args   => {
2088                '_type' => $entry->class,
2089                blog_id => $entry->blog_id,
2090                id      => $entry->id,
2091                ( $is_new ? ( saved_added => 1 ) : ( saved_changes => 1 ) ),
2092                ( $ping_errors ? ( ping_errors => 1 ) : () )
2093            }
2094        )
2095    );
2096}
2097
2098sub ping_continuation {
2099    my $app = shift;
2100    my ( $entry, $blog, %options ) = @_;
2101    my $list = $app->needs_ping(
2102        Entry     => $entry,
2103        Blog      => $blog,
2104        OldStatus => $options{OldStatus}
2105    );
2106    require MT::Entry;
2107    if ( $entry->status == MT::Entry::RELEASE() && $list ) {
2108        my @urls = map { { url => $_ } } @$list;
2109        $app->load_tmpl(
2110            'pinging.tmpl',
2111            {
2112                blog_id    => $blog->id,
2113                entry_id   => $entry->id,
2114                old_status => $options{OldStatus},
2115                is_new     => $options{IsNew},
2116                url_list   => \@urls,
2117            }
2118        );
2119    }
2120    else {
2121        _finish_rebuild_ping( $app, $entry, $options{IsNew} );
2122    }
2123}
2124
2125sub delete {
2126    my $app = shift;
2127    $app->validate_magic() or return;
2128
2129    require MT::Blog;
2130    my $q       = $app->param;
2131    my $blog_id = $q->param('blog_id');
2132    my $blog    = MT::Blog->load($blog_id)
2133        or return $app->error($app->translate('Can\'t load blog #[_1].', $blog_id));
2134
2135    my $can_background =
2136        ( $blog->count_static_templates('Individual') == 0
2137            || MT::Util->launch_background_tasks() ) ? 1 : 0;
2138
2139    my %rebuild_recip;
2140    my $at = $blog->archive_type;
2141    my @at;
2142    if ( $at && $at ne 'None' ) {
2143        my @at_orig = split( /,/, $at );
2144        @at = grep { $_ ne 'Individual' && $_ ne 'Page' } @at_orig;
2145    }
2146
2147    for my $id ( $q->param('id') ) {
2148        my $class = $app->model("entry");
2149        my $obj   = $class->load($id);
2150        return $app->call_return unless $obj;
2151
2152        $app->run_callbacks( 'cms_delete_permission_filter.entry', $app, $obj )
2153          || return $app->error(
2154            $app->translate( "Permission denied: [_1]", $app->errstr() ) );
2155
2156        # Remove Individual archive file.
2157        if ( $app->config('DeleteFilesAtRebuild') ) {
2158            $app->publisher->remove_entry_archive_file( Entry => $obj, );
2159        }
2160        if (   $app->config('RebuildAtDelete')
2161            || $app->config('DeleteFilesAtRebuild') )
2162        {
2163            for my $at (@at) {
2164                my $archiver = $app->publisher->archiver($at);
2165                next unless $archiver;
2166
2167                # Remove archive file if archive file has not entries.
2168                my $to_delete =
2169                    ( $archiver->archive_entries_count( $blog, $at, $obj ) == 1 ) ? 1 : 0
2170                    if $archiver->can('archive_entries_count');
2171                if ( $to_delete && $app->config('DeleteFilesAtRebuild') ) {
2172                     $app->publisher->remove_entry_archive_file(
2173                         Entry       => $obj,
2174                         ArchiveType => $at
2175                     );
2176                }
2177                next if $to_delete;
2178
2179                # Make rebuild recip
2180                if ( $app->config('RebuildAtDelete') ) {
2181                    my ( $start, $end ) = $archiver->date_range( $obj->authored_on )
2182                        if $archiver->date_based() && $archiver->can('date_range');
2183
2184                    if ( $archiver->category_based() ) {
2185                        my $categories = $obj->categories();
2186                        for my $cat (@$categories) {
2187                            if ( $archiver->date_based() ) {
2188                                $rebuild_recip{$at}{ $cat->id }{ $start . $end }
2189                                  {'Start'} = $start;
2190                                $rebuild_recip{$at}{ $cat->id }{ $start . $end }
2191                                  {'End'} = $end;
2192                                $rebuild_recip{$at}{ $cat->id }{ $start . $end }
2193                                  {'File'} =
2194                                  MT::Util::archive_file_for( $obj, $blog, $at,
2195                                    $cat, undef, undef, undef );
2196                            }
2197                            else {
2198                                $rebuild_recip{$at}{ $cat->id }{id} = $cat->id;
2199                                $rebuild_recip{$at}{ $cat->id }{'File'} =
2200                                  MT::Util::archive_file_for( $obj, $blog, $at,
2201                                    $cat, undef, undef, undef );
2202                            }
2203                        }
2204                    }
2205                    elsif ( $archiver->author_based() ) {
2206                        if ( $archiver->date_based() ) {
2207                            $rebuild_recip{$at}{ $obj->author->id }{ $start . $end }
2208                              {'Start'} = $start;
2209                            $rebuild_recip{$at}{ $obj->author->id }{ $start . $end }
2210                              {'End'} = $end;
2211                            $rebuild_recip{$at}{ $obj->author->id }{ $start . $end }
2212                              {'File'} =
2213                              MT::Util::archive_file_for( $obj, $blog, $at, undef,
2214                                undef, undef, $obj->author );
2215                        }
2216                        else {
2217                            $rebuild_recip{$at}{ $obj->author->id }{id} =
2218                              $obj->author->id;
2219                            $rebuild_recip{$at}{ $obj->author->id }{'File'} =
2220                              MT::Util::archive_file_for( $obj, $blog, $at, undef,
2221                                undef, undef, $obj->author );
2222                        }
2223                    }
2224                    elsif ( $archiver->date_based() ) {
2225                        $rebuild_recip{$at}{ $start . $end }{'Start'} = $start;
2226                        $rebuild_recip{$at}{ $start . $end }{'End'}   = $end;
2227                        $rebuild_recip{$at}{ $start . $end }{'File'} =
2228                          MT::Util::archive_file_for( $obj, $blog, $at, undef,
2229                            undef, undef, undef );
2230                    }
2231                    if ( my $prev = $obj->previous(1) ) {
2232                        $rebuild_recip{Individual}{ $prev->id }{id} = $prev->id;
2233                        $rebuild_recip{Individual}{ $prev->id }{'File'} =
2234                          MT::Util::archive_file_for( $prev, $blog, 'Individual',
2235                            undef, undef, undef, undef );
2236                    }
2237                    if ( my $next = $obj->next(1) ) {
2238                        $rebuild_recip{Individual}{ $next->id }{id} = $next->id;
2239                        $rebuild_recip{Individual}{ $next->id }{'File'} =
2240                          MT::Util::archive_file_for( $next, $blog, 'Individual',
2241                            undef, undef, undef, undef );
2242                    }
2243                }
2244            }
2245        }
2246
2247        # Remove object from database
2248        $obj->remove()
2249            or return $app->errtrans(
2250                'Removing [_1] failed: [_2]',
2251                $app->translate('entry'),
2252                $obj->errstr
2253            );
2254        $app->run_callbacks( 'cms_post_delete.entry', $app, $obj );
2255    }
2256
2257    $app->add_return_arg( saved_deleted => 1 );
2258    if ( $q->param('is_power_edit') ) {
2259        $app->add_return_arg( is_power_edit => 1 );
2260    }
2261
2262    if ( $app->config('RebuildAtDelete') ) {
2263        if ($can_background) {
2264            my $res = MT::Util::start_background_task(
2265                sub {
2266                    my $res = $app->rebuild_archives(
2267                        Blog             => $blog,
2268                        Recip            => \%rebuild_recip,
2269                    ) or return $app->publish_error();
2270                    $app->rebuild_indexes( Blog => $blog )
2271                        or return $app->publish_error();
2272                    $app->run_callbacks( 'rebuild', $blog );
2273                    1;
2274                }
2275            );
2276        }
2277        else {
2278            $app->rebuild_archives(
2279                Blog             => $blog,
2280                Recip            => \%rebuild_recip,
2281            ) or return $app->publish_error();
2282            $app->rebuild_indexes( Blog => $blog )
2283                or return $app->publish_error();
2284
2285            $app->run_callbacks( 'rebuild', $blog );
2286        }
2287
2288        $app->add_return_arg( no_rebuild => 1 );
2289        my %params = (
2290            is_full_screen  => 1,
2291            redirect_target => $app->app_path
2292              . $app->script . '?'
2293              . $app->return_args,
2294        );
2295        return $app->load_tmpl( 'rebuilding.tmpl', \%params );
2296    }
2297
2298    return $app->call_return();
2299}
2300
23011;
Note: See TracBrowser for help on using the browser.