root/branches/release-32/lib/MT/CMS/Entry.pm @ 1615

Revision 1615, 83.4 kB (checked in by bchoate, 20 months ago)

Removed 'deferred tag load' mechanism. Fixed jsonification of numeric tags so they are strings too.

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