LJSUP-7813: New thread expander in S2, minimalism
[livejournal.git] / cgi-bin / LJ / S2 / EntryPage.pm
blob03c1d198dd4eea24c40260bbf39aeec07dc19ad4
1 #!/usr/bin/perl
4 use strict;
5 package LJ::S2;
7 use LJ::TimeUtil;
8 use LJ::UserApps;
10 sub EntryPage
12 my ($u, $remote, $opts) = @_;
13 my $get = $opts->{'getargs'};
15 my $p = Page($u, $opts);
16 $p->{'_type'} = "EntryPage";
17 $p->{'view'} = "entry";
18 $p->{'comment_pages'} = undef;
19 $p->{'comments'} = [];
20 $p->{'comment_pages'} = undef;
22 $p->{'view_my_games'} = $remote && $remote->equals($u) && !LJ::SUP->is_remote_sup() && LJ::UserApps->user_games_count($remote);
24 # setup viewall options
25 my ($viewall, $viewsome) = (0, 0);
26 if ($get->{viewall} && LJ::check_priv($remote, 'canview', 'suspended')) {
27 # we don't log here, as we don't know what entry we're viewing yet. the logging
28 # is done when we call EntryPage_entry below.
29 $viewall = LJ::check_priv($remote, 'canview', '*');
30 $viewsome = $viewall || LJ::check_priv($remote, 'canview', 'suspended');
33 my ($entry, $s2entry) = EntryPage_entry($u, $remote, $opts);
34 return if $opts->{'suspendeduser'};
35 return if $opts->{'suspendedentry'};
36 return if $opts->{'readonlyremote'};
37 return if $opts->{'readonlyjournal'};
38 return if $opts->{'handler_return'};
39 return if $opts->{'redir'};
41 $p->{'multiform_on'} = $entry->comments_manageable_by($remote);
43 my $itemid = $entry->jitemid;
44 my $permalink = $entry->url;
45 my $stylemine = $get->{'style'} eq "mine" ? "style=mine" : "";
46 my $style_set = defined $get->{'s2id'} ? "s2id=" . int( $get->{'s2id'} ) : "";
47 my $style_arg = ($stylemine ne '' and $style_set ne '') ? ($stylemine . '&' . $style_set) : ($stylemine . $style_set);
49 if ($u->should_block_robots || $entry->should_block_robots) {
50 $p->{'head_content'} .= LJ::robot_meta_tags();
52 if ($LJ::UNICODE) {
53 $p->{'head_content'} .= '<meta http-equiv="Content-Type" content="text/html; charset='.$opts->{'saycharset'}."\" />\n";
56 # quickreply js libs
57 LJ::need_res(qw(
58 js/basic.js
59 js/json.js
60 js/template.js
61 js/ippu.js
62 js/lj_ippu.js
63 js/userpicselect.js
64 js/hourglass.js
65 js/inputcomplete.js
66 stc/ups.css
67 stc/lj_base.css
68 js/datasource.js
69 js/selectable_table.js
70 )) if ! $LJ::DISABLED{userpicselect} && $remote && $remote->get_cap('userpicselect');
72 LJ::need_res(qw(
73 js/quickreply.js
74 js/md5.js
75 js/thread_expander.js
76 ));
78 if($remote) {
79 LJ::need_string(qw/
80 comment.cancel
81 comment.delete
82 comment.delete.q
83 comment.delete.all
84 comment.delete.all.sub
85 comment.delete.no.options
86 comment.ban.user
87 comment.mark.spam
88 comment.mark.spam.title
89 comment.mark.spam.subject
90 comment.mark.spam.button
91 comment.delete/)
94 $p->{'entry'} = $s2entry;
95 LJ::run_hook('notify_event_displayed', $entry);
97 # add the comments
98 my $view_arg = $get->{'view'} || "";
99 my $flat_mode = ($view_arg =~ /\bflat\b/);
100 my $view_num = ($view_arg =~ /(\d+)/) ? $1 : undef;
102 my %userpic;
103 my %user;
104 my $copts = {
105 'flat' => $flat_mode,
106 'thread' => int($get->{'thread'} / 256),
107 'page' => $get->{'page'},
108 'view' => $view_num,
109 'userpicref' => \%userpic,
110 'userref' => \%user,
111 # user object is cached from call just made in EntryPage_entry
112 'up' => LJ::load_user($s2entry->{'poster'}->{'username'}),
113 'viewall' => $viewall,
114 'expand_all' => $opts->{expand_all},
115 'init_comobj' => 0,
116 'showspam' => $p->{'showspam'} && !$get->{from_rpc},
119 ## Expand all comments on page
120 unless ($LJ::DISABLED{allow_expand_all_comments}){
121 $copts->{expand_all} = 1 if $get->{expand} eq 'all';
124 my $userlite_journal = UserLite($u);
126 my @comments;
127 if ($entry->comments_shown) {
128 ## allow to modify strategies to load/expand comments tree.
129 LJ::run_hooks('load_comments_opts', $u, $itemid, $copts);
130 @comments = LJ::Talk::load_comments($u, $remote, "L", $itemid, $copts);
133 my $tz_remote;
134 if ($remote) {
135 my $tz = $remote->prop("timezone");
136 $tz_remote = $tz ? eval { DateTime::TimeZone->new( name => $tz); } : undef;
139 my $pics = LJ::Talk::get_subjecticons()->{'pic'}; # hashref of imgname => { w, h, img }
140 my $convert_comments = sub {
141 my ($self, $destlist, $srclist, $depth) = @_;
143 foreach my $com (@$srclist) {
144 my $pu = $com->{'posterid'} ? $user{$com->{'posterid'}} : undef;
146 my $dtalkid = $com->{'talkid'} * 256 + $entry->anum;
147 my $text = $com->{'body'};
148 if ($get->{'nohtml'}) {
149 # quote all non-LJ tags
150 $text =~ s{<(?!/?lj)(.*?)>} {&lt;$1&gt;}gi;
152 LJ::CleanHTML::clean_comment(\$text, { 'preformatted' => $com->{'props'}->{'opt_preformatted'},
153 'anon_comment' => (!$pu || $pu->{'journaltype'} eq 'I'),
154 'nocss' => 1,
157 # local time in mysql format to gmtime
158 my $datetime = DateTime_unix($com->{'datepost_unix'});
159 my $datetime_remote = $tz_remote ? DateTime_tz($com->{'datepost_unix'}, $tz_remote) : undef;
160 my $seconds_since_entry = $com->{'datepost_unix'} - $entry->logtime_unix;
161 my $datetime_poster = DateTime_tz($com->{'datepost_unix'}, $pu);
163 my ($edited, $edit_url, $edittime, $edittime_remote, $edittime_poster);
164 if ($com->{_loaded}) {
165 my $comment = LJ::Comment->new($u, jtalkid => $com->{talkid});
167 $edited = $comment->is_edited;
168 $edit_url = LJ::Talk::talkargs($comment->edit_url, $style_arg);
169 if ($edited) {
170 $edittime = DateTime_unix($comment->edit_time);
171 $edittime_remote = $tz_remote ? DateTime_tz($comment->edit_time, $tz_remote) : undef;
172 $edittime_poster = DateTime_tz($comment->edit_time, $pu);
176 my $subject_icon = undef;
177 if (my $si = $com->{'props'}->{'subjecticon'}) {
178 my $pic = $pics->{$si};
179 $subject_icon = Image("$LJ::IMGPREFIX/talk/$pic->{'img'}",
180 $pic->{'w'}, $pic->{'h'}) if $pic;
183 my $comment_userpic;
184 if (my $pic = $userpic{$com->{'picid'}}) {
185 $comment_userpic = Image("$LJ::USERPIC_ROOT/$com->{'picid'}/$pic->{'userid'}",
186 $pic->{'width'}, $pic->{'height'});
189 my $reply_url = LJ::Talk::talkargs($permalink, "replyto=$dtalkid", $style_arg);
191 my $par_url;
193 # in flat mode, promote the parenttalkid_actual
194 if ($flat_mode) {
195 $com->{'parenttalkid'} ||= $com->{'parenttalkid_actual'};
198 if ($com->{'parenttalkid'}) {
199 my $dparent = ($com->{'parenttalkid'} * 256) + $entry->anum;
200 $par_url = LJ::Talk::talkargs($permalink, "thread=$dparent", $style_arg) . "#t$dparent";
203 my $poster;
204 if ($com->{'posterid'} && $pu) {
205 $poster = UserLite($pu);
206 $poster->{'_opt_side_alias'} = 1;
209 # Comment Posted Notice
210 my ($last_talkid, $last_jid) = LJ::get_lastcomment();
211 my $commentposted = "";
212 $commentposted = 1
213 if ($last_talkid == $dtalkid && $last_jid == $remote->{'userid'});
215 my $s2com = {
216 '_type' => 'Comment',
217 'journal' => $userlite_journal,
218 'metadata' => {
219 'picture_keyword' => $com->{'props'}->{'picture_keyword'},
221 'permalink_url' => "$permalink?thread=$dtalkid#t$dtalkid",
222 'reply_url' => $reply_url,
223 'poster' => $poster,
224 'replies' => [],
225 'subject' => LJ::ehtml($com->{'subject'}),
226 'subject_icon' => $subject_icon,
227 'talkid' => $dtalkid,
228 'text' => $text,
229 'userpic' => $comment_userpic,
230 'time' => $datetime,
231 'system_time' => $datetime, # same as regular time for comments
232 'edittime' => $edittime,
233 'tags' => [],
234 'full' => $com->{'_loaded'} ? 1 : 0,
235 'show' => $com->{'_show'} ? 1 : 0,
236 'depth' => $depth,
237 'parent_url' => $par_url,
238 'spam' => $com->{'state'} eq "B" ? 1 : 0,
239 'screened' => $com->{'state'} eq "S" ? 1 : 0,
240 'frozen' => $com->{'state'} eq "F" || !$entry->posting_comments_allowed ? 1 : 0,
241 'deleted' => $com->{'state'} eq "D" ? 1 : 0,
242 'link_keyseq' => [ 'delete_comment' ],
243 'anchor' => "t$dtalkid",
244 'dom_id' => "ljcmt$dtalkid",
245 'comment_posted' => $commentposted,
246 'edited' => $edited ? 1 : 0,
247 'time_remote' => $datetime_remote,
248 'time_poster' => $datetime_poster,
249 'seconds_since_entry' => $seconds_since_entry,
250 'edittime_remote' => $edittime_remote,
251 'edittime_poster' => $edittime_poster,
252 'edit_url' => $edit_url,
255 # don't show info from suspended users, and from users who deleted their journals
256 # and choosed to delete their comments in other journals
257 if (!$viewsome && $pu) {
258 my $hide_comment;
259 if ($pu->is_suspended) {
260 $hide_comment = 1;
261 } elsif ($pu->is_deleted) {
262 my ($purge_comments, $purge_community_entries) = split /:/, $pu->prop("purge_external_content");
263 if ($purge_comments && !$LJ::JOURNALS_WITH_PROTECTED_CONTENT{ $u->{user} }) {
264 $hide_comment = 1;
268 if ($hide_comment) {
269 $s2com->{'text'} = "";
270 $s2com->{'subject'} = "";
271 $s2com->{'full'} = 0;
272 $s2com->{'subject_icon'} = undef;
273 $s2com->{'userpic'} = undef;
277 # Conditionally add more links to the keyseq
278 my $link_keyseq = $s2com->{'link_keyseq'};
279 push @$link_keyseq, $s2com->{'spam'} ? 'unspam_comment' : 'spam_comment';
280 push @$link_keyseq, $s2com->{'screened'} ? 'unscreen_comment' : 'screen_comment';
281 if ($entry->posting_comments_allowed) {
282 push @$link_keyseq, $s2com->{'frozen'} ? 'unfreeze_thread' : 'freeze_thread';
284 push @$link_keyseq, "watch_thread" unless $LJ::DISABLED{'esn'};
285 push @$link_keyseq, "unwatch_thread" unless $LJ::DISABLED{'esn'};
286 push @$link_keyseq, "watching_parent" unless $LJ::DISABLED{'esn'};
287 unshift @$link_keyseq, "edit_comment" if LJ::is_enabled("edit_comments");
289 $s2com->{'thread_url'} = LJ::Talk::talkargs($permalink, "thread=$dtalkid", $style_arg) . "#t$dtalkid";
291 # add the poster_ip metadata if remote user has
292 # access to see it.
293 $s2com->{'metadata'}->{'poster_ip'} = $com->{'props'}->{'poster_ip'} if
294 ($com->{'props'}->{'poster_ip'} && $remote &&
295 ($remote->{'userid'} == $entry->posterid ||
296 ($remote && $remote->can_manage($u)) || $viewall));
298 push @$destlist, $s2com;
300 $self->($self, $s2com->{'replies'}, $com->{'children'}, $depth+1);
303 $p->{'comments'} = [];
304 $convert_comments->($convert_comments, $p->{'comments'}, \@comments, ($get->{'depth'} || 0) + 1);
306 # prepare the javascript data structure to put in the top of the page
307 # if the remote user is a manager of the comments
308 my $do_commentmanage_js = $p->{'multiform_on'};
309 if ($LJ::DISABLED{'commentmanage'}) {
310 if (ref $LJ::DISABLED{'commentmanage'} eq "CODE") {
311 $do_commentmanage_js = $LJ::DISABLED{'commentmanage'}->($remote);
312 } else {
313 $do_commentmanage_js = 0;
317 # print comment info
319 my $canAdmin = ($remote && ($remote->can_manage($u) || $remote->can_sweep($u))) ? 1 : 0;
320 my $formauth = LJ::ejs(LJ::eurl(LJ::form_auth(1)));
322 my $cmtinfo = {
323 form_auth => $formauth,
324 journal => $u->user,
325 canAdmin => $canAdmin,
326 remote => $remote ? $remote->user : undef,
329 my $recurse = sub {
330 my ($self, $array, $depth) = @_;
332 foreach my $i (@$array) {
333 my $cmt = LJ::Comment->new($u, dtalkid => $i->{talkid});
335 my $has_threads = scalar @{$i->{'replies'}};
336 my $poster = $i->{'poster'} ? $i->{'poster'}{'username'} : "";
337 my @child_ids = map { $_->{'talkid'} } @{$i->{'replies'}};
338 my $parent = $cmt->parent;
339 $cmtinfo->{$i->{talkid}} = {
340 rc => \@child_ids,
341 u => $poster,
342 username => $i->{'poster'} ? $i->{'poster'}->{'_u'}->{'user'} : '',
343 parent => $parent && $parent->valid ? $parent->dtalkid : undef,
344 full => ($i->{full}),
345 depth => $depth,
347 $self->($self, $i->{'replies'}, $depth + 1) if $has_threads;
351 $recurse->($recurse, $p->{'comments'}, 0);
353 my $js = "<script>\n// don't crawl this. read http://www.livejournal.com/developer/exporting.bml\n";
354 $js .= "var LJ_cmtinfo = " . LJ::js_dumper($cmtinfo) . "\n";
355 $js .= '</script>';
356 $p->{'LJ_cmtinfo'} = $js if $opts->{'need_cmtinfo'};
357 $p->{'head_content'} .= $js;
360 my %meta = (
361 'title' => LJ::Text->drop_html($entry->subject_raw),
362 'description' => LJ::Text->drop_html($entry->event_raw),
365 $p->{'head_content'} .= qq[
366 <meta name="title" value="$meta{'title'}"/>
367 <meta name="description" value="$meta{'description'}"/>
370 LJ::need_res(qw(
371 js/commentmanage.js
374 $p->{'_stylemine'} = $get->{'style'} eq 'mine' ? 1 : 0;
375 $p->{'_picture_keyword'} = $get->{'prop_picture_keyword'};
377 $p->{'viewing_thread'} = $get->{'thread'} ? 1 : 0;
379 # default values if there were no comments, because
380 # LJ::Talk::load_comments() doesn't provide them.
381 if ($copts->{'out_error'} eq 'noposts' || scalar @comments < 1) {
382 $copts->{'out_pages'} = $copts->{'out_page'} = 1;
383 $copts->{'out_items'} = 0;
384 $copts->{'out_itemfirst'} = $copts->{'out_itemlast'} = undef;
387 $p->{'comment_pages'} = ItemRange({
388 'all_subitems_displayed' => ($copts->{'out_pages'} == 1),
389 'current' => $copts->{'out_page'},
390 'from_subitem' => $copts->{'out_itemfirst'},
391 'num_subitems_displayed' => scalar @comments,
392 'to_subitem' => $copts->{'out_itemlast'},
393 'total' => $copts->{'out_pages'},
394 'total_subitems' => $copts->{'out_items'},
395 '_url_of' => sub {
396 my $sty = $flat_mode ? "view=flat&" : "";
397 return "$permalink?${sty}page=" . int($_[0]) .
398 ($style_arg ? "&$style_arg" : '');
402 return $p;
405 sub EntryPage_entry
407 my ($u, $remote, $opts) = @_;
409 my $get = $opts->{'getargs'};
411 my $uri = LJ::Request->uri;
413 my ($ditemid, $itemid);
414 my $entry = $opts->{ljentry}; # only defined in named-URI case. otherwise undef.
416 unless ($entry || $uri =~ /(\d+)\.html/) {
417 $opts->{'handler_return'} = 404;
418 LJ::Request->pnotes ('error' => 'e404');
419 LJ::Request->pnotes ('remote' => LJ::get_remote());
420 return;
423 $entry ||= LJ::Entry->new($u, ditemid => $1);
425 unless ($entry and $entry->correct_anum) {
426 $opts->{'handler_return'} = 404;
427 LJ::Request->pnotes ('error' => 'e404');
428 LJ::Request->pnotes ('remote' => LJ::get_remote());
429 return;
432 $ditemid = $entry->ditemid;
433 $itemid = $entry->jitemid;
435 my $pu = $entry->poster;
437 my $userlite_journal = UserLite($u);
438 my $userlite_poster = UserLite($pu);
440 # do they have the viewall priv?
441 my $canview = $get->{'viewall'} && LJ::check_priv($remote, "canview");
442 my ($viewall, $viewsome) = (0, 0);
443 if ($canview) {
444 LJ::statushistory_add($u->{'userid'}, $remote->{'userid'},
445 "viewall", "entry: $u->{'user'}, itemid: $itemid, statusvis: $u->{'statusvis'}");
446 $viewall = LJ::check_priv($remote, 'canview', '*');
447 $viewsome = $viewall || LJ::check_priv($remote, 'canview', 'suspended');
450 # check using normal rules
451 unless ($entry->visible_to($remote, { viewall => $canview })) {
452 if ($remote) {
453 $opts->{'handler_return'} = 403;
454 LJ::Request->pnotes ('error' => 'private');
455 LJ::Request->pnotes ('remote' => LJ::get_remote());
456 return;
457 } else {
458 my $host = LJ::Request->header_in("Host");
459 my $args = scalar LJ::Request->args;
460 my $querysep = $args ? "?" : "";
461 my $redir = LJ::eurl("http://$host$uri$querysep$args");
462 $opts->{'redir'} = "$LJ::SITEROOT/?returnto=$redir&errmsg=notloggedin";
463 return;
467 if (($pu && $pu->{'statusvis'} eq 'S') && !$viewsome) {
468 $opts->{'suspendeduser'} = 1;
469 LJ::Request->pnotes ('error' => 'suspended');
470 LJ::Request->pnotes ('remote' => LJ::get_remote());
471 return;
474 if ($entry && $entry->is_suspended_for($remote)) {
475 $opts->{'suspendedentry'} = 1;
476 LJ::Request->pnotes ('error' => 'suspended_post');
477 LJ::Request->pnotes ('remote' => LJ::get_remote());
478 return;
481 my $replycount = $entry->prop("replycount");
482 my $nc = "";
483 $nc .= "nc=$replycount" if $replycount && $remote && $remote->{'opt_nctalklinks'};
485 my $stylemine = $get->{'style'} eq "mine" ? "style=mine" : "";
486 my $style_set = defined $get->{'s2id'} ? "s2id=" . int( $get->{'s2id'} ) : "";
487 my $style_arg = ($stylemine ne '' and $style_set ne '') ? ($stylemine . '&' . $style_set) : ($stylemine . $style_set);
488 my $show_spam_arg = ($get->{'mode'} ne "showspam" and LJ::is_enabled('spam_button')) ? "mode=showspam" : "";
490 my $userpic = Image_userpic($pu, $entry->userpic ? $entry->userpic->picid : 0);
492 my $permalink = $entry->url;
493 my $readurl = LJ::Talk::talkargs($permalink, $nc, $style_arg);
494 my $readspamurl = LJ::Talk::talkargs($permalink, $nc, $style_arg, $show_spam_arg);
495 my $posturl = LJ::Talk::talkargs($permalink, "mode=reply", $style_arg);
497 my $comments = CommentInfo({
498 'read_url' => $readurl,
499 'read_spam_url' => LJ::Talk::can_unmark_spam($remote, $u, $pu) ? $readspamurl : '',
500 'spam_counter' => $entry->prop('spam_counter') || 0,
501 'post_url' => $posturl,
502 'count' => $replycount,
503 'maxcomments' => ($replycount >= LJ::get_cap($u, 'maxcomments')) ? 1 : 0,
504 'enabled' => $entry->comments_shown,
505 'locked' => !$entry->posting_comments_allowed,
506 'screened' => ($entry->prop("hasscreened") and $remote && LJ::Talk::can_view_screened($remote, $u)),
509 $comments->{show_postlink} = $entry->posting_comments_allowed;
510 $comments->{show_readlink} = $entry->comments_shown && ($replycount || $comments->{'screened'});
512 # load tags
513 my @taglist;
515 my $tag_map = $entry->tag_map;
516 while (my ($kwid, $kw) = each %$tag_map) {
517 push @taglist, Tag($u, $kwid => $kw);
519 LJ::run_hooks('augment_s2_tag_list', u => $u, jitemid => $itemid, tag_list => \@taglist);
520 @taglist = sort { $a->{name} cmp $b->{name} } @taglist;
523 my $subject = $entry->subject_html;
525 my $no_cut_expand = !$get->{cut_expand} && $get->{page} && $get->{page} > 1 ? 1 : 0;
527 my $event = $entry->event_html({
528 no_cut_expand => $no_cut_expand,
529 page => $get->{page},
532 if ($get->{'nohtml'}) {
533 # quote all non-LJ tags
534 $subject =~ s{<(?!/?lj)(.*?)>} {&lt;$1&gt;}gi;
535 $event =~ s{<(?!/?lj)(.*?)>} {&lt;$1&gt;}gi;
538 if ($opts->{enable_tags_compatibility} && @taglist) {
539 $event .= LJ::S2::get_tags_text($opts->{ctx}, \@taglist);
542 if ($entry->security eq "public") {
543 $LJ::REQ_GLOBAL{'text_of_first_public_post'} = $event;
545 if (@taglist) {
546 $LJ::REQ_GLOBAL{'tags_of_first_public_post'} = [map { $_->{name} } @taglist];
549 my @verticals = $entry->verticals_list_for_ad;
550 if (@verticals) {
551 $LJ::REQ_GLOBAL{'verticals_of_first_public_post'} = join(",", @verticals);
555 my $s2entry = Entry($u, {
556 'subject' => $subject,
557 'text' => $event,
558 'dateparts' => LJ::TimeUtil->alldatepart_s2($entry->eventtime_mysql),
559 'system_dateparts' => LJ::TimeUtil->alldatepart_s2($entry->logtime_mysql),
560 'security' => $entry->security,
561 'allowmask' => $entry->allowmask,
562 'props' => $entry->props,
563 'itemid' => $ditemid,
564 'comments' => $comments,
565 'journal' => $userlite_journal,
566 'poster' => $userlite_poster,
567 'tags' => \@taglist,
568 'new_day' => 0,
569 'end_day' => 0,
570 'userpic' => $userpic,
571 'permalink_url' => $permalink,
574 return ($entry, $s2entry);