LJSUP-17669: Login.bml form refactoring
[livejournal.git] / cgi-bin / LJ / Subscription.pm
blob5a2d67b2bb90286e3f7b275ae8cc3a1ffdfaa936
1 package LJ::Subscription;
2 use strict;
3 use Carp qw(croak confess cluck);
4 use Class::Autouse qw(
5 LJ::NotificationMethod
6 LJ::Typemap
7 LJ::Event
8 LJ::Subscription::Pending
9 LJ::Subscription::Group
12 use constant {
13 INACTIVE => 1 << 0, # user has deactivated
16 my @subs_fields = qw(userid subid is_dirty journalid etypeid arg1 arg2
17 ntypeid createtime expiretime flags);
19 sub new_by_id {
20 my ($class, $u, $subid) = @_;
21 croak "new_by_id requires a valid 'u' object"
22 unless LJ::isu($u);
23 return if $u->is_expunged;
25 croak "invalid subscription id passed"
26 unless defined $subid && int($subid) > 0;
28 my $row = $u->selectrow_hashref
29 ("SELECT userid, subid, is_dirty, journalid, etypeid, " .
30 "arg1, arg2, ntypeid, createtime, expiretime, flags " .
31 "FROM subs WHERE userid=? AND subid=?", undef, $u->{userid}, $subid);
32 die $u->errstr if $u->err;
34 return $class->new_from_row($row);
37 sub dump {
38 my ($self) = @_;
40 return $self->id if $self->id && $self->id != 0;
42 my %props = map { $_ => $self->{$_} } @subs_fields;
43 return \%props;
46 sub new_from_dump {
47 my ($class, $u, $dump) = @_;
49 return $class->new_by_id($u, $dump) if ref $dump eq '';
50 return bless($dump, $class);
53 sub freeze {
54 my $self = shift;
55 return "subid-" . $self->owner->{userid} . '-' . $self->id;
58 # can return either a LJ::Subscription or LJ::Subscription::Pending object
59 sub thaw {
60 my ($class, $data, $u, $POST) = @_;
62 # valid format?
63 return undef unless ($data =~ /^(pending|subid) - $u->{userid} .+ ?(-old)?$/x);
65 my ($type, $userid, $subid) = split("-", $data);
67 return LJ::Subscription::Pending->thaw($data, $u, $POST) if $type eq 'pending';
68 die "Invalid subscription data type: $type" unless $type eq 'subid';
70 unless ($u) {
71 my $subuser = LJ::load_userid($userid);
72 die "no user" unless $subuser;
73 $u = LJ::get_authas_user($subuser);
74 die "Invalid user $subuser->{user}" unless $u;
77 return $class->new_by_id($u, $subid);
80 sub pending { 0 }
81 sub default_selected { $_[0]->active }
83 sub query_user_subscriptions {
84 my ($class, $u, %filters) = @_;
85 croak "subscriptions_of_user requires a valid 'u' object"
86 unless LJ::isu($u);
88 return if $u->is_expunged;
90 my $dbh = LJ::get_cluster_reader($u) or die "cannot get a DB handle";
92 my (@conditions, @binds);
94 push @conditions, 1;
96 foreach my $prop (qw(journalid ntypeid etypeid flags arg1 arg2)) {
97 next unless defined $filters{$prop};
98 push @conditions, "$prop=?";
99 push @binds, $filters{$prop};
102 my $conditions = join(' AND ', @conditions);
103 return $dbh->selectall_arrayref(
105 SELECT
106 userid, subid, is_dirty, journalid, etypeid,
107 arg1, arg2, ntypeid, createtime, expiretime, flags
108 FROM subs
109 WHERE userid=? AND $conditions
110 }, { Slice => {} }, $u->id, @binds
114 sub subscriptions_of_user {
115 my ($class, $u) = @_;
117 croak "subscriptions_of_user requires a valid 'u' object"
118 unless LJ::isu($u);
120 return if $u->is_expunged;
122 return @{$u->{_subscriptions}} if $u->{_subscriptions};
124 my @subs;
126 my $val = LJ::MemCache::get('subscriptions:' . $u->id);
127 if (defined $val) {
128 my @ints = unpack("N*", $val);
129 for (my $i = 0; $i < scalar(@ints); $i += 11) {
130 my %row;
131 @row{@subs_fields} = @ints[$i..$i+10];
132 push @subs, $class->new_from_row(\%row);
134 } else {
135 @subs = map { $class->new_from_row($_) }
136 @{ $class->query_user_subscriptions($u) };
138 my @ints;
139 foreach my $sub (@subs) {
140 my %row = %$sub;
141 push @ints, @row{@subs_fields};
143 LJ::MemCache::set('subscriptions:' . $u->id, pack("N*", @ints));
146 @{$u->{_subscriptions}} = @subs;
147 return @subs;
150 # Class method
151 # Look for a subscription matching the parameters: journalu/journalid,
152 # ntypeid/method, event/etypeid, arg1, arg2
153 # Returns a list of subscriptions for this user matching the parameters
155 # For pages with tons of comments search is divided in two stages:
156 # I prefetch => 1
157 # II postprocess => [$a, $b, $c]
158 # First stage is done once for request and is needed to fetch and filter out
159 # active user subscriptions in current journal
160 # Second stage is done for every comment on page and contains all additional filtering conditions
161 sub find {
162 my ($class, $u, %params) = @_;
164 my ($etypeid, $ntypeid, $arg1, $arg2, $flags, $prefetch, $postprocess);
166 $prefetch = delete $params{'prefetch'};
167 $postprocess = delete $params{'postprocess'};
169 if (my $evt = delete $params{event}) {
170 $etypeid = LJ::Event->event_to_etypeid($evt);
173 if (my $nmeth = delete $params{method}) {
174 $ntypeid = LJ::NotificationMethod->method_to_ntypeid($nmeth);
177 $etypeid ||= delete $params{etypeid};
178 $ntypeid ||= delete $params{ntypeid};
180 $flags = delete $params{flags};
182 my $journalid = delete $params{journalid};
183 $journalid ||= LJ::want_userid(delete $params{journal}) if defined $params{journal};
185 $arg1 = delete $params{arg1};
186 $arg2 = delete $params{arg2};
188 my $require_active = delete $params{require_active} ? 1 : 0;
190 croak "Invalid parameters passed to ${class}->find" if keys %params;
192 return () if defined $arg1 && $arg1 =~ /\D/;
193 return () if defined $arg2 && $arg2 =~ /\D/;
195 my @subs;
196 if ( $postprocess ) {
197 @subs = @$postprocess;
198 } else {
199 @subs = $class->subscriptions_of_user($u);
202 unless ( $postprocess ) {
203 # This filter should be at first place because we need not to check
204 # subscriptions in other journals and this can save us from tons of work later
205 # Matters on pages with comments, when both journalid and require_active
206 # conditions are active
207 @subs = grep { $_->journalid == $journalid } @subs if defined $journalid;
209 @subs = grep { $_->active } @subs if $require_active;
212 return @subs if $prefetch;
214 # filter subs on each parameter
215 @subs = grep { $_->ntypeid == $ntypeid } @subs if $ntypeid;
216 @subs = grep { $_->{etypeid} == $etypeid } @subs if $etypeid;
217 @subs = grep { $_->flags == $flags } @subs if defined $flags;
219 @subs = grep { $_->{arg1} == $arg1 } @subs if defined $arg1;
220 @subs = grep { $_->{arg2} == $arg2 } @subs if defined $arg2;
222 return @subs;
225 # Instance method
226 # Deactivates a subscription. If this is not a "tracking" subscription,
227 # it will delete it instead.
228 sub deactivate {
229 my $self = shift;
231 my %opts = @_;
232 my $force = delete $opts{force}; # force-delete
234 croak "Invalid args" if scalar keys %opts;
236 my $subid = $self->id
237 or croak "Invalid subsciption";
239 my $u = $self->owner;
241 # if it's the inbox method, deactivate/delete the other notification methods too
242 my @to_remove = ();
244 my @subs = $self->corresponding_subs;
246 foreach my $subscr (@subs) {
247 # Don't deactivate if the Inbox is always subscribed to
248 my $always_checked = $subscr->event_class->always_checked ? 1 : 0;
249 unless ($force) {
250 # delete non-inbox methods if we're deactivating
251 if ($subscr->method eq 'LJ::NotificationMethod::Inbox' && !$always_checked) {
252 $subscr->_deactivate;
253 } else {
254 $subscr->delete;
256 } else {
257 $subscr->delete;
262 # deletes a subscription
263 sub delete {
264 my $self = shift;
265 my $u = $self->owner;
267 my @subs = $self->corresponding_subs;
268 foreach my $subscr (@subs) {
269 $u->do("DELETE FROM subs WHERE subid=? AND userid=?", undef, $subscr->id, $u->id);
272 # delete from cache in user
273 undef $u->{_subscriptions};
275 $self->invalidate_cache($u);
277 return 1;
280 # class method, nukes all subs for a user
281 sub delete_all_subs {
282 my ($class, $u) = @_;
284 return if $u->is_expunged;
285 $u->do("DELETE FROM subs WHERE userid = ?", undef, $u->id);
286 undef $u->{_subscriptions};
288 $class->invalidate_cache($u);
290 return 1;
293 # class method, nukes all inactive subs for a user
294 sub delete_all_inactive_subs {
295 my ($class, $u, $dryrun) = @_;
297 return if $u->is_expunged;
299 my $set = LJ::Subscription::GroupSet->fetch_for_user($u);
300 my @inactive_groups = grep { !$_->active } $set->groups;
302 unless ($dryrun) {
303 $set->drop_group($_) foreach (@inactive_groups);
306 return scalar(@inactive_groups);
309 # find matching subscriptions with different notification methods
310 sub corresponding_subs {
311 my $self = shift;
313 my @subs = ($self);
315 if ($self->method eq 'LJ::NotificationMethod::Inbox') {
316 push @subs, $self->owner->find_subscriptions(
317 journalid => $self->journalid,
318 etypeid => $self->etypeid,
319 arg1 => $self->arg1,
320 arg2 => $self->arg2,
324 return @subs;
327 # Class method
328 sub new_from_row {
329 my ($class, $row) = @_;
331 return undef unless $row;
332 my $self = bless {%$row}, $class;
333 # TODO validate keys of row.
334 return $self;
337 sub create {
338 my ($class, $u, %args) = @_;
340 # easier way for eveenttype
341 if (my $evt = delete $args{'event'}) {
342 $args{etypeid} = LJ::Event->event_to_etypeid($evt);
345 # easier way to specify ntypeid
346 if (my $ntype = delete $args{'method'}) {
347 $args{ntypeid} = LJ::NotificationMethod->method_to_ntypeid($ntype);
350 # easier way to specify journal
351 if (my $ju = delete $args{'journal'}) {
352 $args{journalid} = $ju->{userid}
353 if !$args{journalid} && $ju;
356 $args{arg1} ||= 0;
357 $args{arg2} ||= 0;
359 $args{journalid} ||= 0;
361 foreach (qw(ntypeid etypeid)) {
362 croak "Required field '$_' not found in call to $class->create" unless defined $args{$_};
364 foreach (qw(userid subid createtime)) {
365 croak "Can't specify field '$_'" if defined $args{$_};
368 my ($existing) = grep {
369 $args{etypeid} == $_->{etypeid} &&
370 $args{ntypeid} == $_->{ntypeid} &&
371 $args{journalid} == $_->{journalid} &&
372 $args{arg1} == $_->{arg1} &&
373 $args{arg2} == $_->{arg2} &&
374 $args{flags} == $_->{flags}
375 } $class->subscriptions_of_user($u);
377 return $existing
378 if defined $existing;
380 my $subid = LJ::alloc_user_counter($u, 'E')
381 or die "Could not alloc subid for user $u->{user}";
383 $args{subid} = $subid;
384 $args{userid} = $u->{userid};
385 $args{createtime} = time();
387 my $self = $class->new_from_row( \%args );
389 my @fields;
390 foreach (@subs_fields) {
391 if (exists( $args{$_} )) {
392 push @fields, { name => $_, value => delete $args{$_} };
396 croak( "Extra args defined, (" . join( ', ', keys( %args ) ) . ")" ) if keys %args;
398 # DELETE FROM subs records with all selected field values
399 # without 'subid', 'flags' and 'createtime'.
400 my $sql_filter = sub { $_->{'name'} !~ /subid|flags|createtime/ };
402 my $sth = $u->prepare( 'DELETE FROM subs WHERE ' .
403 join( ' AND ', map { $_->{'name'} . '=?' } grep { $sql_filter->($_) } @fields )
406 $sth->execute( map { $_->{'value'} } grep { $sql_filter->($_) } @fields );
408 $sth = $u->prepare(
409 'INSERT INTO subs (' . join( ',', map { $_->{'name'} } @fields ) . ')' .
410 'VALUES (' . join( ',', map {'?'} @fields ) . ')' );
412 $sth->execute( map { $_->{'value'} } @fields );
413 LJ::errobj($u)->throw if $u->err;
415 push @{$u->{_subscriptions}}, $self;
417 $self->invalidate_cache($u);
419 return $self;
422 # returns a hash of arguments representing this subscription (useful for passing to
423 # other functions, such as find)
424 sub sub_info {
425 my $self = shift;
426 return (
427 journalid => $self->journalid,
428 etypeid => $self->etypeid,
429 ntypeid => $self->ntypeid,
430 arg1 => $self->arg1,
431 arg2 => $self->arg2,
432 flags => $self->flags,
436 # returns a nice HTML description of this current subscription
437 sub as_html {
438 my $self = shift;
440 my $evtclass = LJ::Event->class($self->etypeid);
441 return undef unless $evtclass;
442 return $evtclass->subscription_as_html($self);
445 sub activate {
446 my $self = shift;
447 $self->clear_flag(INACTIVE);
450 sub _deactivate {
451 my $self = shift;
452 $self->set_flag(INACTIVE);
455 sub set_flag {
456 my ($self, $flag) = @_;
458 my $flags = $self->flags;
460 # don't bother if flag already set
461 return if $flags & $flag;
463 $flags |= $flag;
465 if ($self->owner && ! $self->pending) {
466 $self->owner->do("UPDATE subs SET flags = flags | ? WHERE userid=? AND subid=?", undef,
467 $flag, $self->owner->userid, $self->id);
468 die $self->owner->errstr if $self->owner->errstr;
470 $self->{flags} = $flags;
471 delete $self->owner->{_subscriptions};
475 sub clear_flag {
476 my ($self, $flag) = @_;
478 my $flags = $self->flags;
480 # don't bother if flag already cleared
481 return unless $flags & $flag;
483 # clear the flag
484 $flags &= ~$flag;
487 if ($self->owner && ! $self->pending) {
488 $self->owner->do("UPDATE subs SET flags = flags & ~? WHERE userid=? AND subid=?", undef,
489 $flag, $self->owner->userid, $self->id);
490 die $self->owner->errstr if $self->owner->errstr;
492 $self->{flags} = $flags;
493 delete $self->owner->{_subscriptions};
497 sub id {
498 my $self = shift;
500 return $self->{subid};
503 sub createtime {
504 my $self = shift;
505 return $self->{createtime};
508 sub flags {
509 my $self = shift;
510 return $self->{flags} || 0;
513 sub active {
514 my $self = shift;
515 return ! ($self->flags & INACTIVE);
518 sub expiretime {
519 my $self = shift;
520 return $self->{expiretime};
523 sub journalid {
524 my $self = shift;
525 return $self->{journalid};
528 sub journal {
529 my $self = shift;
530 return LJ::load_userid($self->{journalid});
533 sub arg1 {
534 my $self = shift;
535 return $self->{arg1};
538 sub arg2 {
539 my $self = shift;
540 return $self->{arg2};
543 sub ntypeid {
544 my $self = shift;
545 return $self->{ntypeid};
548 sub method {
549 my $self = shift;
550 return LJ::NotificationMethod->class($self->ntypeid);
553 sub notify_class {
554 my $self = shift;
555 return LJ::NotificationMethod->class($self->{ntypeid});
558 sub etypeid {
559 my $self = shift;
560 return $self->{etypeid};
563 sub event_class {
564 my $self = shift;
565 return LJ::Event->class($self->{etypeid});
568 # returns the owner (userid) of the subscription
569 sub userid {
570 my $self = shift;
571 return $self->{userid};
574 sub owner {
575 my $self = shift;
576 return LJ::load_userid($self->{userid});
579 sub dirty {
580 my $self = shift;
581 return $self->{is_dirty};
584 sub notification {
585 my $subscr = shift;
586 my $class = LJ::NotificationMethod->class($subscr->{ntypeid});
588 my $note;
589 if ($LJ::DEBUG{'official_post_esn'} && $subscr->etypeid == LJ::Event::OfficialPost->etypeid) {
590 # we had (are having) some problems with subscriptions to millions of people, so
591 # this exists for now for debugging that, without actually emailing/inboxing
592 # those people while we debug
593 $note = LJ::NotificationMethod::DebugLog->new_from_subscription($subscr, $class);
594 } else {
595 $note = $class->new_from_subscription($subscr);
598 return $note;
601 sub process {
602 my ($self, $opts, @events) = @_;
603 my $note = $self->notification or return;
605 return 1 if $self->etypeid == LJ::Event::OfficialPost->etypeid && $LJ::DISABLED{"officialpost_esn"};
607 # process events for unauthorised users;
608 return 1 if LJ::Event->class($self->etypeid) eq 'LJ::Event::SupportRequest' &&
609 !$self->{userid};
611 # significant events (such as SecurityAttributeChanged) must be processed even for inactive users.
612 return 1
613 unless $self->notify_class->configured_for_user($self->owner)
614 || LJ::Event->class($self->etypeid)->is_significant;
616 return $note->notify($opts, @events);
619 sub unique {
620 my $self = shift;
622 my $note = $self->notification or return undef;
623 return $note->unique . ':' . $self->owner->{user};
626 # returns true if two subscriptions are equivilant
627 sub equals {
628 my ($self, $other) = @_;
630 return 1 if $self->id == $other->id;
632 my $match = $self->ntypeid == $other->ntypeid &&
633 $self->etypeid == $other->etypeid && $self->flags == $other->flags;
635 $match &&= $other->arg1 && ($self->arg1 == $other->arg1) if $self->arg1;
636 $match &&= $other->arg2 && ($self->arg2 == $other->arg2) if $self->arg2;
638 $match &&= $self->journalid == $other->journalid;
640 return $match;
643 sub available_for_user {
644 my ($self, $u) = @_;
646 $u ||= $self->owner;
648 return $self->group->event->available_for_user($u);
651 sub group {
652 my ($self) = @_;
653 return LJ::Subscription::Group->group_from_sub($self);
656 sub event {
657 my ($self) = @_;
658 return $self->group->event;
661 sub enabled {
662 my ($self) = @_;
664 my $ret = $self->group->enabled;
665 return $ret;
668 sub invalidate_cache {
669 my ($class, $u) = @_;
670 LJ::MemCache::delete('subscriptions:'.$u->id);
671 LJ::MemCache::delete('subscriptions_count:'.$u->id);
674 package LJ::Error::Subscription::TooMany;
675 sub fields { qw(subscr u); }
677 sub as_html { $_[0]->as_string }
678 sub as_string {
679 my $self = shift;
680 my $max = $self->field('u')->get_cap('subscriptions');
681 return 'The subscription "' . $self->field('subscr')->as_html . '" was not saved because you have' .
682 " reached your limit of $max active subscriptions. Subscriptions need to be deactivated before more can be added.";
685 # Too many subscriptions exist, not necessarily active
686 package LJ::Error::Subscription::TooManySystemMax;
687 sub fields { qw(subscr u max); }
689 sub as_html { $_[0]->as_string }
690 sub as_string {
691 my $self = shift;
692 my $max = $self->field('max');
693 return 'The subscription "' . $self->field('subscr')->as_html . '" was not saved because you have' .
694 " more than $max existing subscriptions. Subscriptions need to be completely removed before more can be added.";