root/branches/release-34/lib/MT/CMS/Entry.pm @ 1823

Revision 1823, 84.1 kB (checked in by takayama, 20 months ago)

Fixed BugId:67959
* Added check for result of object loading

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