3 package LJ
::UniqCookie
;
9 my %req_cache_uid2uniqs = (); # uid => [ uniq1, uniq2, ... ]
10 my %req_cache_uniq2uids = (); # uniq => [ uid1, uid2, ... ]
12 # number of uniq cookies to keep in cache + db before being cleaned
13 my $window_size = 1000;
15 sub clear_request_cache
{
18 %req_cache_uid2uniqs = ();
19 %req_cache_uniq2uids = ();
22 sub set_request_cache_by_user
{
24 my ($u_arg, $uniq_list) = @_;
26 my $uid = LJ
::want_userid
($u_arg)
27 or croak
"invalid user arg: $u_arg";
29 croak
"invalid uniq list: $uniq_list"
30 unless ref $uniq_list eq 'ARRAY';
32 return $req_cache_uid2uniqs{$uid} = $uniq_list;
35 sub get_request_cache_by_user
{
39 my $uid = LJ
::want_userid
($u_arg)
40 or croak
"invalid user arg: $u_arg";
42 return $req_cache_uid2uniqs{$uid};
45 sub set_request_cache_by_uniq
{
47 my ($uniq, $user_list) = @_;
49 croak
"invalid uniq arg: $uniq"
52 croak
"invalid user list: $user_list"
53 unless ref $user_list eq 'ARRAY';
56 foreach my $u_arg (@
$user_list) {
57 my $uid = LJ
::want_userid
($u_arg)
58 or croak
"invalid arg in user_list: $u_arg";
63 $req_cache_uniq2uids{$uniq} = \
@userids;
66 sub get_request_cache_by_uniq
{
69 croak
"invalid 'uniq' arg: $uniq"
72 return $req_cache_uniq2uids{$uniq};
75 sub delete_memcache_by_user
{
79 my $uid = LJ
::want_userid
($u_arg)
80 or croak
"invalid user arg: $u_arg";
82 LJ
::MemCache
::delete("uid2uniqs:$uid");
85 sub delete_memcache_by_uniq
{
88 croak
"invalid 'uniq' arg: $uniq"
91 LJ
::MemCache
::delete("uniq2uids:$uniq");
94 sub set_memcache_by_user
{
96 my ($u_arg, $uniq_list) = @_;
98 my $uid = LJ
::want_userid
($u_arg)
99 or croak
"invalid user arg: $u_arg";
101 # we store uid => [] and uniq => [], so defined but false
102 # is okay as a value of these memcache keys, but not as part of the key
104 LJ
::MemCache
::set
("uid2uniqs:$uid" => $uniq_list, $exptime);
107 sub get_memcache_by_user
{
111 my $uid = LJ
::want_userid
($u_arg)
112 or die "invalid user arg: $u_arg";
114 return LJ
::MemCache
::get
("uid2uniqs:$uid");
117 sub set_memcache_by_uniq
{
119 my ($uniq, $user_list) = @_;
121 croak
"invalid 'uniq' argument: $uniq"
124 croak
"invalid user list: $user_list"
125 unless ref $user_list eq 'ARRAY';
128 foreach my $u_arg (@
$user_list) {
129 my $uid = LJ
::want_userid
($u_arg)
130 or croak
"invalid arg in user_list: $u_arg";
135 # we store uid => [] and uniq => [], so defined but false
136 # is okay as a value of these memcache keys, but not as part of the key
138 LJ
::MemCache
::set
("uniq2uids:$uniq" => \
@userids, $exptime);
141 sub get_memcache_by_uniq
{
144 croak
"invalid 'uniq' argument: $uniq"
147 return LJ
::MemCache
::get
("uniq2uids:$uniq");
150 # straight db request instead of loading is used, because we need pure results
151 # and do not need any uniqs to be cleaned
152 sub get_shared_uniqs
{
153 my ($uid, $threshold) = @_;
157 my $dbr = LJ
::get_db_reader
();
158 my $rows = $dbr->selectall_arrayref(qq{
159 SELECT m2
.userid AS uid
,
160 count
(m2
.uniq
) AS qty
161 FROM uniqmap m1 INNER JOIN uniqmap m2
168 }, { Slice
=> {} },$uid, $threshold);
175 return if $class->is_disabled;
177 my ($uniq, $uid_arg) = @_; # no extra parts, only ident
178 return unless length $uniq;
180 my $uid = LJ
::want_userid
($uid_arg);
181 croak
"invalid userid arg: $uid_arg"
184 my $dbh = LJ
::get_db_writer
()
185 or die "unable to contact uniq master for uniq mapping";
187 # allow tests to specify an insertion time callback which specifies
188 # how we calculate insertion times for rows
189 my $time_sql = "UNIX_TIMESTAMP()";
190 if ($LJ::_T_UNIQCOOKIE_MODTIME_CB
) {
191 $time_sql = int($LJ::_T_UNIQCOOKIE_MODTIME_CB
->($uniq, $uid));
194 my $rv = $dbh->do("REPLACE INTO uniqmap SET uniq=?, userid=?, modtime=$time_sql",
196 die $dbh->errstr if $dbh->err;
198 # clear memcache so its next query will reflect our changes
199 $class->delete_memcache_by_uniq($uniq);
200 $class->delete_memcache_by_user($uid);
202 # also clear request cache
203 $class->clear_request_cache;
205 # we clean on cache misses in ->load_mapping, but we also want
206 # to randomly clean on write actions so that we don't end up
207 # with users who write many rows but for some reason never
208 # load any rows, and are therefore never cleaned
209 if ($class->should_lazy_clean) {
211 $class->load_mapping( user
=> $uid );
212 # no need for uniq => $uniq case
219 sub should_lazy_clean
{
225 if ($LJ::_T_UNIQCOOKIE_LAZY_CLEAN_PCT
) {
226 $pct = $LJ::_T_UNIQCOOKIE_LAZY_CLEAN_PCT
;
229 return rand() <= $pct;
235 my $remote = LJ
::get_remote
();
236 my $uniq = $class->current_uniq;
238 return 1 unless $LJ::UNIQ_COOKIES
;
239 return LJ
::conf_test
($LJ::DISABLED
{uniq_mapping
}, $remote, $uniq);
245 my $uniq = $class->current_uniq;
248 my $uid = $class->load_mapping( uniq
=> $uniq );
249 return LJ
::load_userid
($uid);
252 # if 'uniq' passed in, returns mapped userid
253 # if 'remote' passed in, returns mapped uniq
256 return if $class->is_disabled;
260 my $uniq = delete $opts{uniq
};
261 my $user = delete $opts{user
};
264 return wantarray() ?
@_ : $_[0];
268 my $uid = LJ
::want_userid
($user)
269 or croak
"invalid user arg: $user";
271 return $ret->($class->_load_mapping_uid($uid, %opts));
275 return $ret->($class->_load_mapping_uniq($uniq, %opts));
278 croak
"must load mapping via 'uniq' or 'user'";
281 sub _load_mapping_uid
{
286 # first, check request cache
287 my $cache_val = $class->get_request_cache_by_user($uid);
288 return @
$cache_val if defined $cache_val;
290 # second, check memcache
291 my $memval = $class->get_memcache_by_user($uid);
293 $class->set_request_cache_by_user($uid => $memval);
297 my $dbh = LJ
::get_db_writer
()
298 or die "unable to contact uniq writer";
300 my $limit = $window_size + 1;
301 my $sth = $dbh->prepare
302 ("SELECT uniq, modtime FROM uniqmap WHERE userid=? " .
303 "ORDER BY modtime DESC LIMIT $limit");
305 die $dbh->errstr if $dbh->err;
307 my (@uniq_list, $min_modtime);
308 while (my ($curr_uniq, $modtime) = $sth->fetchrow_array) {
309 push @uniq_list, $curr_uniq;
310 $min_modtime = $modtime if ! $min_modtime || $modtime < $min_modtime;
313 # we got out more rows than we allow after cleaning, so an insert
314 # has happened ... we'll clean that now
316 if (@uniq_list >= $limit) {
317 $delete_ct = $dbh->do("DELETE FROM uniqmap WHERE userid=? AND modtime<=?",
318 undef, $uid, $min_modtime);
320 @uniq_list = @uniq_list[0..$window_size-1];
323 # allow tests to register a callback to determine
324 # how many rows were deleted
325 if (ref $LJ::_T_UNIQCOOKIE_DELETE_CB
) {
326 $LJ::_T_UNIQCOOKIE_DELETE_CB
->('userid', $delete_ct);
330 # now set the value we retrieved in both memcache values
331 $class->set_request_cache_by_user($uid => \
@uniq_list);
332 $class->set_memcache_by_user($uid => \
@uniq_list);
337 sub _load_mapping_uniq
{
341 # first, check request cache
342 my $cache_val = $class->get_request_cache_by_uniq($uniq);
343 return @
$cache_val if defined $cache_val;
345 # second, check memcache
346 my $memval = $class->get_memcache_by_uniq($uniq);
348 $class->set_request_cache_by_uniq($uniq => $memval);
352 my $dbh = LJ
::get_db_writer
()
353 or die "unable to contact uniq reader";
355 my $limit = $window_size + 1;
356 my $sth = $dbh->prepare
357 ("SELECT userid, modtime FROM uniqmap WHERE uniq=? " .
358 "ORDER BY modtime DESC LIMIT $limit");
359 $sth->execute($uniq);
360 die $dbh->errstr if $dbh->err;
362 my (@uid_list, $min_modtime);
363 while (my ($curr_uid, $modtime) = $sth->fetchrow_array) {
364 push @uid_list, $curr_uid;
365 $min_modtime = $modtime if ! $min_modtime || $modtime < $min_modtime;
368 # we got out more rows than we allow after cleaning, so an insert
369 # has happened ... we'll clean that now
371 if (@uid_list >= $limit) {
372 $delete_ct = $dbh->do("DELETE FROM uniqmap WHERE uniq=? AND modtime<=?",
373 undef, $uniq, $min_modtime);
375 # trim the cached/returned value as well
376 @uid_list = @uid_list[0..$window_size-1];
379 # allow tests to register a callback to determine
380 # how many rows were deleted
381 if (ref $LJ::_T_UNIQCOOKIE_DELETE_CB
) {
382 $LJ::_T_UNIQCOOKIE_DELETE_CB
->('uniq', $delete_ct);
385 # now set the value we retrieved in both memcache values
386 $class->set_request_cache_by_uniq($uniq => \
@uid_list);
387 $class->set_memcache_by_uniq($uniq => \
@uid_list);
392 sub generate_uniq_ident
{
395 return LJ
::rand_chars
(15);
398 ###############################################################################
399 # These methods require web context, they deal with Apache->request and cookies
402 sub ensure_cookie_value
{
404 return unless LJ
::is_web_context
();
406 return unless LJ
::Request
->is_inited;
408 ## do not set cookie from some domains:
411 ## userpic, l-userpic
413 ## these are domains with public, mostly static content.
415 my $domain = LJ
::Request
->header_in("Host");
416 my ($subdomain) = $domain =~ m
|^(.+?
)\
.\Q
$LJ::DOMAIN\E
|;
417 return if $subdomain and $LJ::COOKIE_FREE_DOMAINS
{$subdomain};
418 return if LJ
::Request
->uri() =~ m
|^/robots
.txt
|;
420 my ($uniq, $uniq_time, $uniq_extra) = $class->parts_from_cookie;
422 # set this uniq as our current
423 # -- will be overridden later if we generate a new value
424 $class->set_current_uniq($uniq);
427 =head LJSUP
-8676: set ljuniq cookie on client side
. it allows to cached pages
for logged
-out users
.
428 # if no cookie, create one. if older than a day, revalidate
430 return if $uniq && $now - $uniq_time < 86400;
435 $uniq = $class->generate_uniq_ident;
438 my $new_cookie_value = "$uniq:$now";
439 my $hook_saved_mapping = 0;
440 if (LJ
::are_hooks
('transform_ljuniq_value')) {
441 $new_cookie_value = LJ
::run_hook
442 ('transform_ljuniq_value',
443 { value
=> $new_cookie_value,
444 extra
=> $uniq_extra,
445 hook_saved_mapping
=> \
$hook_saved_mapping});
447 # if it changed the actual uniq identifier (first part)
449 $uniq = $class->parts_from_value($new_cookie_value);
453 # set this new or transformed uniq in Apache request notes
454 $class->set_current_uniq($uniq);
456 if ($setting_new && ! $hook_saved_mapping && ! $class->is_disabled) {
457 my $remote = LJ
::get_remote
();
458 $class->save_mapping($uniq => $remote) if $remote;
461 # set uniq cookies for all cookie_domains
462 my @domains = ref $LJ::COOKIE_DOMAIN ? @
$LJ::COOKIE_DOMAIN
: ($LJ::COOKIE_DOMAIN
);
463 foreach my $dom (@domains) {
464 LJ
::Request
->err_headers_out->add("Set-Cookie" =>
465 "ljuniq=$new_cookie_value; " .
466 "expires=" . LJ
::TimeUtil
->time_to_cookie($now + 86400*60) . "; " .
467 ($dom ?
"domain=$dom; " : "") . "path=/");
474 sub sysban_should_block
{
476 return 0 unless LJ
::is_web_context
();
478 my $uri = LJ
::Request
->uri;
479 return 0 if ( $LJ::BLOCKED_BOT_URI
&& index( $uri, $LJ::BLOCKED_BOT_URI
) == 0 );
481 # if cookie exists, check for sysban
482 if (my @cookieparts = $class->parts_from_cookie) {
483 my ($uniq, $uniq_time, $uniq_extra) = @cookieparts;
484 return 1 if LJ
::sysban_check
( 'uniq', $uniq );
490 # returns: (uniq_val, uniq_time, uniq_extra)
491 sub parts_from_cookie
{
493 return unless LJ
::is_web_context
();
495 my $cookieval = LJ
::Request
->header_in("Cookie");
497 if ($cookieval =~ /\bljuniq\s*=\s*([a-zA-Z0-9]{15})(?:\:|\%3A)(\d+)([^;]+)/) {
498 my ($uniq, $uniq_time, $uniq_extra) = ($1, $2, $3);
499 $uniq_extra =~ s/\%3A/:/g;
500 return wantarray() ?
($uniq, $uniq_time, $uniq_extra) : $uniq;
506 # returns: (uniq_val, uniq_time, uniq_extra)
507 sub parts_from_value
{
511 if ($value =~ /^([a-zA-Z0-9]{15}):(\d+)(.+)$/) {
512 return wantarray() ?
($1, $2, $3) : $1;
518 sub set_current_uniq
{
522 $LJ::REQ_CACHE
{current_uniq
} = $uniq;
524 return unless LJ
::is_web_context
();
526 LJ
::Request
->notes('uniq' => $uniq);
534 if ($LJ::_T_UNIQCOOKIE_CURRENT_UNIQ
) {
535 return $LJ::_T_UNIQCOOKIE_CURRENT_UNIQ
;
538 # should be in $LJ::REQ_CACHE, so return from
540 my $val = $LJ::REQ_CACHE
{current_uniq
};
543 # otherwise, legacy place is in $r->notes
544 return unless LJ
::is_web_context
();
546 # see if a uniq is set for this request
547 # -- this accounts for cases when the cookie was initially
548 # set in this request, so it wasn't received in an
549 # incoming headerno cookie was sent in
550 $val = LJ
::Request
->notes('uniq');
553 $val = $class->parts_from_cookie;