# This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. package LJ::User; use strict; no warnings 'uninitialized'; use Carp; ######################################################################## ### 11. Birthdays and Age-Related Functions ### FIXME: Some of these may be outdated when we remove under-13 accounts. =head2 Birthdays and Age-Related Functions =cut # Users age based off their profile birthdate sub age { my $u = shift; croak "Invalid user object" unless LJ::isu($u); my $bdate = $u->{bdate}; return unless length $bdate; my ( $year, $mon, $day ) = $bdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)/; my $age = LJ::calc_age( $year, $mon, $day ); return $age if defined $age && $age > 0; return; } # This will format the birthdate based on the user prop sub bday_string { my $u = shift; croak "invalid user object passed" unless LJ::isu($u); my $bdate = $u->{'bdate'}; my ( $year, $mon, $day ) = split( /-/, $bdate ); my $bday_string = ''; if ( $u->can_show_full_bday && $day > 0 && $mon > 0 && $year > 0 ) { $bday_string = $bdate; } elsif ( $u->can_show_bday && $day > 0 && $mon > 0 ) { $bday_string = "$mon-$day"; } elsif ( $u->can_show_bday_year && $year > 0 ) { $bday_string = $year; } $bday_string =~ s/^0000-//; return $bday_string; } # Returns the best guess age of the user, which is init_age if it exists, otherwise age sub best_guess_age { my $u = shift; return 0 unless $u->is_person || $u->is_identity; return $u->init_age || $u->age; } # returns if this user can join an adult community or not # adultref will hold the value of the community's adult content flag sub can_join_adult_comm { my ( $u, %opts ) = @_; return 1 unless LJ::is_enabled('adult_content'); my $adultref = $opts{adultref}; my $comm = $opts{comm} or croak "No community passed"; my $adult_content = $comm->adult_content_calculated; $$adultref = $adult_content; return 0 if $adult_content eq "explicit" && ( $u->is_minor || !$u->best_guess_age ); return 1; } # Birthday logic -- should a notification be sent? # Currently the same logic as can_show_bday with an exception for # journals that have memorial or deleted status. sub can_notify_bday { my ( $u, %opts ) = @_; croak "invalid user object passed" unless LJ::isu($u); return 0 if $u->is_memorial; return 0 if $u->is_deleted; return $u->can_show_bday(%opts); } # Birthday logic -- can any of the birthday info be shown # This will return true if any birthday info can be shown sub can_share_bday { my ( $u, %opts ) = @_; croak "invalid user object passed" unless LJ::isu($u); my $with_u = $opts{with} || LJ::get_remote(); return 0 if $u->opt_sharebday eq 'N'; return 0 if $u->opt_sharebday eq 'R' && !$with_u; return 0 if $u->opt_sharebday eq 'F' && !$u->trusts($with_u); return 1; } # Birthday logic -- show appropriate string based on opt_showbday # This will return true if the actual birthday can be shown sub can_show_bday { my ( $u, %opts ) = @_; croak "invalid user object passed" unless LJ::isu($u); my $to_u = $opts{to} || LJ::get_remote(); return 0 unless $u->can_share_bday( with => $to_u ); return 0 unless $u->opt_showbday eq 'D' || $u->opt_showbday eq 'F'; return 1; } # This will return true if the actual birth year can be shown sub can_show_bday_year { my ( $u, %opts ) = @_; croak "invalid user object passed" unless LJ::isu($u); my $to_u = $opts{to} || LJ::get_remote(); return 0 unless $u->can_share_bday( with => $to_u ); return 0 unless $u->opt_showbday eq 'Y' || $u->opt_showbday eq 'F'; return 1; } # This will return true if month, day, and year can be shown sub can_show_full_bday { my ( $u, %opts ) = @_; croak "invalid user object passed" unless LJ::isu($u); my $to_u = $opts{to} || LJ::get_remote(); return 0 unless $u->can_share_bday( with => $to_u ); return 0 unless $u->opt_showbday eq 'F'; return 1; } sub include_in_age_search { my $u = shift; # if they don't display the year return 0 if $u->opt_showbday =~ /^[DN]$/; # if it's not visible to registered users return 0 if $u->opt_sharebday =~ /^[NF]$/; return 1; } # This returns the users age based on the init_bdate (users coppa validation birthdate) sub init_age { my $u = shift; croak "Invalid user object" unless LJ::isu($u); my $init_bdate = $u->prop('init_bdate'); return unless $init_bdate; my ( $year, $mon, $day ) = $init_bdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)/; my $age = LJ::calc_age( $year, $mon, $day ); return $age if $age > 0; return; } # return true if we know user is a minor (< 18) sub is_minor { my $self = shift; my $age = $self->best_guess_age; return 0 unless $age; return 1 if ( $age < 18 ); return 0; } sub next_birthday { my $u = shift; return if $u->is_expunged; return $u->selectrow_array( "SELECT nextbirthday FROM birthdays " . "WHERE userid = ?", undef, $u->id ) + 0; } # class method, loads next birthdays for a bunch of users sub next_birthdays { my $class = shift; # load the users we need, so we can get their clusters my $clusters = LJ::User->split_by_cluster(@_); my %bdays = (); foreach my $cid ( keys %$clusters ) { next unless $cid; my @users = @{ $clusters->{$cid} || [] }; my $dbcr = LJ::get_cluster_def_reader($cid) or die "Unable to load reader for cluster: $cid"; my $bind = join( ",", map { "?" } @users ); my $sth = $dbcr->prepare("SELECT * FROM birthdays WHERE userid IN ($bind)"); $sth->execute(@users); while ( my $row = $sth->fetchrow_hashref ) { $bdays{ $row->{userid} } = $row->{nextbirthday}; } } return \%bdays; } # opt_showbday options # F - Full Display of Birthday # D - Only Show Month/Day DEFAULT # Y - Only Show Year # N - Do not display sub opt_showbday { my $u = shift; # option not set = "yes", set to N = "no" $u->_lazy_migrate_infoshow; # migrate above did nothing # -- if user was already migrated in the past, we'll # fall through and show their prop value # -- if user not migrated yet, we'll synthesize a prop # value from infoshow without writing it unless ( LJ::is_enabled('infoshow_migrate') || $u->{allow_infoshow} eq ' ' ) { return $u->{allow_infoshow} eq 'Y' ? undef : 'N'; } if ( $u->raw_prop('opt_showbday') =~ /^(D|F|N|Y)$/ ) { return $u->raw_prop('opt_showbday'); } else { return 'D'; } } # opt_sharebday options # A - All people # R - Registered Users # F - Trusted Only # N - Nobody sub opt_sharebday { my $u = shift; if ( $u->raw_prop('opt_sharebday') =~ /^(A|F|N|R)$/ ) { return $u->raw_prop('opt_sharebday'); } else { return 'F' if $u->is_minor; return 'A'; } } # this sets the unix time of their next birthday for notifications sub set_next_birthday { my $u = shift; return if $u->is_expunged; my ( $year, $mon, $day ) = split( /-/, $u->{bdate} ); unless ( $mon > 0 && $day > 0 ) { $u->do( "DELETE FROM birthdays WHERE userid = ?", undef, $u->id ); return; } my $as_unix = sub { return LJ::mysqldate_to_time( sprintf( "%04d-%02d-%02d", @_ ) ); }; my $curyear = ( gmtime(time) )[5] + 1900; # Calculate the time of their next birthday. # Assumption is that birthday-notify jobs won't be backed up. # therefore, if a user's birthday is 1 day from now, but # we process notifications for 2 days in advance, their next # birthday is really a year from tomorrow. # We need to do calculate three possible "next birthdays": # Current Year + 0: For the case where we it for the first # time, which could happen later this year. # Current Year + 1: For the case where we're setting their next # birthday on (approximately) their birthday. Gotta set it for # next year. This works in all cases but... # Current Year + 2: For the case where we're processing notifs # for next year already (eg, 2 days in advance, and we do # 1/1 birthdays on 12/30). Year + 1 gives us the date two days # from now! So, add another year on top of that. # We take whichever one is earliest, yet still later than the # window of dates where we're processing notifications. my $bday; for my $inc ( 0 .. 2 ) { $bday = $as_unix->( $curyear + $inc, $mon, $day ); last if $bday > time() + $LJ::BIRTHDAY_NOTIFS_ADVANCE; } # up to twelve hours drift so we don't get waves $bday += int( rand( 12 * 3600 ) ); $u->do( "REPLACE INTO birthdays VALUES (?, ?)", undef, $u->id, $bday ); die $u->errstr if $u->err; return $bday; } sub should_fire_birthday_notif { my $u = shift; return 0 unless $u->is_person; return 0 unless $u->is_visible; # if the month/day can't be shown return 0 if $u->opt_showbday =~ /^[YN]$/; # if the birthday isn't shown to anyone return 0 if $u->opt_sharebday eq "N"; # note: this isn't intended to capture all cases where birthday # info is restricted. we want to pare out as much as possible; # individual "can user X see this birthday" is handled in # LJ::Event::Birthday->matches_filter return 1; } # data for generating packed directory records sub usersearch_age_with_expire { my $u = shift; croak "Invalid user object" unless LJ::isu($u); # don't include their age in directory searches # if it's not publicly visible in their profile my $age = $u->include_in_age_search ? $u->age : 0; $age += 0; # no need to expire due to age if we don't have a birthday my $expire = $u->next_birthday || undef; return ( $age, $expire ); } ######################################################################## ### 12. Adult Content Functions =head2 Adult Content Functions =cut # defined by the user # returns 'none', 'concepts' or 'explicit' sub adult_content { my $u = shift; my $prop_value = $u->prop('adult_content'); return $prop_value ? $prop_value : "none"; } # uses user-defined prop to figure out the adult content level sub adult_content_calculated { my $u = shift; return $u->adult_content; } # returns who marked the entry as the 'adult_content_calculated' adult content level sub adult_content_marker { my $u = shift; return "journal"; } # defuned by the user sub adult_content_reason { my $u = shift; return $u->prop('adult_content_reason'); } sub hide_adult_content { my $u = shift; my $prop_value = $u->prop('hide_adult_content'); if ( !$u->best_guess_age ) { return "concepts"; } if ( $u->is_minor && $prop_value ne "concepts" ) { return "explicit"; } return $prop_value ? $prop_value : "none"; } # returns a number that represents the user's chosen search filtering level # 0 = no filtering # 1-10 = moderate filtering # >10 = strict filtering sub safe_search { my $u = shift; my $prop_value = $u->prop('safe_search'); # current user 18+ default is 0 # current user <18 default is 10 # new user default (prop value is "nu_default") is 10 return 0 if $prop_value eq "none"; return $prop_value if $prop_value && $prop_value =~ /^\d+$/; return 0 if $prop_value ne "nu_default" && $u->best_guess_age && !$u->is_minor; return 10; } # determine if the user in "for_u" should see $u in a search result sub should_show_in_search_results { my ( $u, %opts ) = @_; # check basic user attributes first return 0 unless $u->is_visible; return 0 if $u->is_person && $u->age && $u->age < 14; # now check adult content / safe search return 1 unless LJ::is_enabled('adult_content') && LJ::is_enabled('safe_search'); my $adult_content = $u->adult_content_calculated; my $for_u = $opts{for}; # only show accounts with no adult content to logged out users return $adult_content eq "none" ? 1 : 0 unless LJ::isu($for_u); my $safe_search = $for_u->safe_search; return 1 if $safe_search == 0; # user wants to see everyone # calculate the safe_search level for this account my $adult_content_flag = $LJ::CONTENT_FLAGS{$adult_content}; my $adult_content_flag_level = $adult_content_flag ? $adult_content_flag->{safe_search_level} : 0; # if the level is set, see if it exceeds the desired safe_search level return 1 unless $adult_content_flag_level; return ( $safe_search < $adult_content_flag_level ) ? 1 : 0; } 1;