Tatsuki SUGIURA
sugi****@users*****
2006年 7月 12日 (水) 20:41:47 JST
Index: slashjp/plugins/Daypass/Daypass.pm diff -u /dev/null slashjp/plugins/Daypass/Daypass.pm:1.1 --- /dev/null Wed Jul 12 20:41:47 2006 +++ slashjp/plugins/Daypass/Daypass.pm Wed Jul 12 20:41:46 2006 @@ -0,0 +1,315 @@ +# This code is a part of Slash, and is released under the GPL. +# Copyright 1997-2005 by Open Source Technology Group. See README +# and COPYING for more information, or see http://slashcode.com/. +# $Id: Daypass.pm,v 1.1 2006/07/12 11:41:46 sugi Exp $ + +package Slash::Daypass; + +use strict; +use Slash::Utility; +use Slash::DB::Utility; +use Apache::Cookie; +use vars qw($VERSION); +use base 'Slash::DB::Utility'; + +($VERSION) = ' $Revision: 1.1 $ ' =~ /\$Revision:\s+([^\s]+)/; + +# FRY: And where would a giant nerd be? THE LIBRARY! + +################################################################# +sub new { + my($class, $user) = @_; + my $self = {}; + + my $plugin = getCurrentStatic('plugin'); + return unless $plugin->{Daypass}; + + bless($self, $class); + $self->{virtual_user} = $user; + $self->sqlConnect(); + + return $self; +} + +################################################################# +{ # closure +my $_getDA_cache; +my $_getDA_cached_nextcheck; +sub getDaypassesAvailable { + my($self) = @_; + my $constants = getCurrentStatic(); + + if (!$_getDA_cache + || !$_getDA_cached_nextcheck + || $_getDA_cached_nextcheck <= time()) { + + $_getDA_cached_nextcheck = time() + ($constants->{daypass_cache_expire} || 300); + if (!$constants->{daypass_offer_method}) { + # (Re)load the cache from a reader DB. + my $reader = getObject('Slash::DB', { db_type => 'reader' }); + $_getDA_cache = $reader->sqlSelectAllHashrefArray( + "daid, adnum, minduration, + UNIX_TIMESTAMP(starttime) AS startts, UNIX_TIMESTAMP(endtime) AS endts, + aclreq", + "daypass_available"); + } else { + my $pos = $constants->{daypass_offer_method1_adpos} || 31; + my $regex = $constants->{daypass_offer_method1_regex} || '!placeholder'; + my $acl = $constants->{daypass_offer_method1_acl} || ''; + my $minduration = $constants->{daypass_offer_method1_minduration} || 10; + my $avail = $self->checkAdposRegex($pos, $regex); + if ($avail) { + my $adnum = $constants->{daypass_adnum} || 13; + $_getDA_cache = [ { + daid => 999, # dummy placeholder, not used + adnum => $adnum, + minduration => $minduration, + startts => time - 60, + endts => time + 3600, + aclreq => $acl, + } ]; + } else { + $_getDA_cache = [ ]; + } + } + + } + + return $_getDA_cache; +} +} # end closure + +sub checkAdposRegex { + my($self, $pos, $regex) = @_; + my $ad_text = getAd($pos); + return 0 if !$ad_text; + my $neg = 0; + if (substr($regex, 0, 1) eq '!') { + # Strip off leading char. + $neg = 1; + $regex = substr($regex, 1); + } + my $avail = ($ad_text =~ /$regex/) ? 1 : 0; + $avail = !$avail if $neg; + return $avail; +} + +sub getDaypass { + my($self) = @_; + + my $constants = getCurrentStatic(); + return undef unless $constants->{daypass}; + + my $da_ar = $self->getDaypassesAvailable(); + return undef if !$da_ar || !@$da_ar; + + # There are one or more rows in the table, which might mean there + # are one or more daypass ads that we can show. + my @ads_available = ( ); + my $time = time(); + my $user = undef; + for my $hr (@$da_ar) { + next unless $hr->{startts} <= $time; + next unless $time <= $hr->{endts}; + if ($constants->{daypass_offer_onlytologgedin}) { + $user ||= getCurrentUser(); + next unless $user && !$user->{is_anon}; + } + if ($hr->{aclreq}) { + $user ||= getCurrentUser(); + print STDERR scalar(localtime) . " $$ cannot get user in getDaypass\n" if !$user; + next unless $user && !$user->{is_anon} + && $user->{acl}{ $hr->{aclreq} }; + } + push @ads_available, $hr; + } + + return undef unless @ads_available; + + # Return a random one. + return $ads_available[rand(@ads_available)]; +} + +sub createDaypasskey { + my($self, $dp_hr) = @_; + + # If no daypass was available, we can't return a key. + return "" if !$dp_hr; + + # How far in the future before this daypass can be confirmed? + # I.e. how much of the ad do we insist the user watch? + my $secs_ahead = $dp_hr->{minduration} || 0; + # Give the user a break of 1 second, to allow for clock drift + # or what-have-you. + $secs_ahead -= 1; + + my $key = getAnonId(1, 20); + my $rows = $self->sqlInsert('daypass_keys', { + daypasskey => $key, + -key_given => 'NOW()', + -earliest_confirmable => "DATE_ADD(NOW(), INTERVAL $secs_ahead SECOND)", + key_confirmed => undef, + }); + if ($rows < 1) { + return ""; + } else { + return $key; + } +} + +sub confirmDaypasskey { + my($self, $key) = @_; + my $constants = getCurrentStatic(); + + my $key_q = $self->sqlQuote($key); + + my $rows = $self->sqlUpdate( + "daypass_keys", + { -key_confirmed => "NOW()" }, + "daypasskey = $key_q + AND earliest_confirmable <= NOW() + AND key_confirmed IS NULL"); + + my $confcode = ""; + if ($rows > 0) { + $confcode = getAnonId(1, 20); + my $hr = { + confcode => $confcode, + gooduntil => $self->getGoodUntil(), + }; + $rows = $self->sqlInsert('daypass_confcodes', $hr); + } + + $rows = 0 if $rows < 1; + return $rows ? $confcode : 0; +} + +sub getDaypassTZOffset { + my($self) = @_; + my $slashdb = getCurrentDB(); + my $constants = getCurrentStatic(); + + my $dptz = $constants->{daypass_tz} || 'GMT'; + return 0 if $dptz eq 'GMT'; + + my $timezones = $slashdb->getTZCodes(); + return 0 unless $timezones && $timezones->{$dptz}; + return $timezones->{$dptz}{off_set} || 0; +} + +sub getGoodUntil { + my($self) = @_; + my $slashdb = getCurrentDB(); + + # The business decision made here is that all daypasses expire + # at the same time, midnight in some timezone. This seems to + # make more sense than having different users' daypasses expire + # at different times. I'm not really happy about putting + # business logic in this .pm file but I doubt this decision + # will change. But if it does, here's the line of code that + # needs to change. + my $off_set = $self->getDaypassTZOffset() || 0; + + # Determine the final second on the day for the timezone in + # question, expressed in GMT time. If the timezone is EST, for + # which the offset is -18000 seconds, we determine this by bumping + # the current GMT datetime -18000 seconds, taking the GMT date, + # appending the time 23:59:59 to it, and re-adding +18000 to + # that time. + my $gmt_end_of_tz_day = + $off_set + ? $slashdb->sqlSelect("DATE_SUB( + CONCAT( + SUBSTRING( + DATE_ADD( NOW(), INTERVAL $off_set SECOND ), + 1, 10 + ), + ' 23:59:59' + ), + INTERVAL $off_set SECOND)") + : $slashdb->sqlSelect("CONCAT(SUBSTRING(NOW(), 1, 10), ' 23:59:59')"); + # If there was an error of some kind, note it and at least + # return a legal value. + if (!$gmt_end_of_tz_day) { + errorLog("empty gmt_end_of_tz_day '$off_set' " . time); + $gmt_end_of_tz_day = '0000-00-00 00:00:00'; + } + return $gmt_end_of_tz_day; +} + +sub userHasDaypass { + my($self, $user) = @_; + my $form = getCurrentForm(); + return 0 unless $ENV{GATEWAY_INTERFACE}; + my $cookies = Apache::Cookie->fetch; + return 0 unless $cookies && $cookies->{daypassconfcode}; + my $confcode = $cookies->{daypassconfcode}->value(); + my $confcode_q = $self->sqlQuote($confcode); + + # We really should memcached this. But the query cache + # will take a lot of the sting out of it. + my $gooduntil = $self->sqlSelect( + 'UNIX_TIMESTAMP(gooduntil)', + 'daypass_confcodes', + "confcode=$confcode_q"); + # If it's expired, it's no good. + $gooduntil = 0 if $gooduntil && $gooduntil < time(); + return $gooduntil ? 1 : 0; +} + +sub doOfferDaypass { + my($self) = @_; + # If daypasses are entirely turned off, or the var indicating whether + # to offer daypasses is set to false, then no. + my $constants = getCurrentStatic(); + return 0 unless $constants->{daypass} && $constants->{daypass_offer}; + # If the user is a subscriber, then no. + my $user = getCurrentUser(); + return 0 if $user->{is_subscriber}; + # If the user already has a daypass, then no. + return 0 if $self->userHasDaypass($user); + # If there are no ads available for this user, then no. + my $dp_hr = $self->getDaypass(); + return 0 unless $dp_hr; + # Otherwise, yes. Return its daid. + return $dp_hr->{daid}; +} + +sub getOfferText { + my($self) = @_; + my $constants = getCurrentStatic(); + my $text = ""; + if (!$constants->{daypass_offer_method}) { + $text = Slash::getData('offertext', {}, 'daypass'); + } else { + my $pos = $constants->{daypass_offer_method1_adpos} || 31; + $text = getAd($pos); + } + return $text; +} + +################################################################# +sub DESTROY { + my($self) = @_; + $self->{_dbh}->disconnect if $self->{_dbh} && !$ENV{GATEWAY_INTERFACE}; +} + +1; + +=head1 NAME + +Slash::Daypass - Slash Daypass module + +=head1 SYNOPSIS + + use Slash::Daypass; + +=head1 DESCRIPTION + +This contains all of the routines currently used by Daypass. + +=head1 SEE ALSO + +Slash(3). + +=cut Index: slashjp/plugins/Daypass/Makefile.PL diff -u /dev/null slashjp/plugins/Daypass/Makefile.PL:1.1 --- /dev/null Wed Jul 12 20:41:47 2006 +++ slashjp/plugins/Daypass/Makefile.PL Wed Jul 12 20:41:46 2006 @@ -0,0 +1,8 @@ +use ExtUtils::MakeMaker; +# See lib/ExtUtils/MakeMaker.pm for details of how to influence +# the contents of the Makefile that is written. +WriteMakefile( + 'NAME' => 'Slash::Daypass', + 'VERSION_FROM' => 'Daypass.pm', # finds $VERSION + 'PM' => { 'Daypass.pm' => '$(INST_LIBDIR)/Daypass.pm' }, +); Index: slashjp/plugins/Daypass/PLUGIN diff -u /dev/null slashjp/plugins/Daypass/PLUGIN:1.1 --- /dev/null Wed Jul 12 20:41:47 2006 +++ slashjp/plugins/Daypass/PLUGIN Wed Jul 12 20:41:46 2006 @@ -0,0 +1,9 @@ +# $Id: PLUGIN,v 1.1 2006/07/12 11:41:46 sugi Exp $ +name=Daypass +description="Daypass" +htdoc=daypass.pl +mysql_dump=mysql_dump.sql +mysql_schema=mysql_schema.sql +template=templates/data;daypass;default +template=templates/main;daypass;default + Index: slashjp/plugins/Daypass/daypass.pl diff -u /dev/null slashjp/plugins/Daypass/daypass.pl:1.1 --- /dev/null Wed Jul 12 20:41:47 2006 +++ slashjp/plugins/Daypass/daypass.pl Wed Jul 12 20:41:46 2006 @@ -0,0 +1,112 @@ +#!/usr/bin/perl -w +# This code is a part of Slash, and is released under the GPL. +# Copyright 1997-2005 by Open Source Technology Group. See README +# and COPYING for more information, or see http://slashcode.com/. +# $Id: daypass.pl,v 1.1 2006/07/12 11:41:46 sugi Exp $ + +use strict; +use Slash; +use Slash::Display; +use Slash::Utility; + +################################################################## +sub main { + my $gSkin = getCurrentSkin(); + my $daypass_reader = getObject('Slash::Daypass', { db_type => 'reader' }); + my $dps = $daypass_reader->getDaypassesAvailable(); + if (!$dps || !@$dps) { + redirect($gSkin->{rootdir}); + } + + my $daypass_writer = getObject('Slash::Daypass'); + my $form = getCurrentForm(); + my $dpk = $form->{dpk} || ""; + if ($dpk) { + # Strip this form field. + $dpk =~ /^(\w+)$/; + $dpk = $1 || ""; + } + + my($adnum, $minduration) = (0, 0); + + if ($dpk) { + + my $confcode = $daypass_writer->confirmDaypasskey($dpk); + if ($confcode) { + # Do the housekeeping required to echo the + # conf code out to the client's browser, + # and then don't continue with the rest of + # this function (in particular, don't create a + # new key). + key_confirmed($confcode); + return ; + } + # The user probably didn't watch enough of the + # ad. Let them keep watching! + print STDERR scalar(localtime) . " daypass.pl $$ apparently early click\n"; + $adnum = $form->{adnum}; + $adnum =~ /^(\d+)$/; + $adnum = $1 || 0; + if (!$adnum) { + # We don't know which ad they were watching (they + # probably edited the URL) so fetch a new one. +print STDERR scalar(localtime) . " daypass.pl $$ no adnum found, refetching\n"; + $dpk = ""; + } + + } + + if (!$dpk) { + + my $dp_hr = $daypass_reader->getDaypass(); + if (!$dp_hr) { + # Something went wrong. We don't have a daypass for + # the user to see. +print STDERR scalar(localtime) . " daypass.pl $$ cannot choose daypass\n"; + redirect($gSkin->{rootdir}); + return ; + } + $dpk = $daypass_writer->createDaypasskey($dp_hr); + if (!$dpk) { + # Something went wrong. We can't show the user a key. +print STDERR scalar(localtime) . " daypass.pl $$ cannot show key\n"; + redirect($gSkin->{rootdir}); + return ; + } + $adnum = $dp_hr->{adnum}; + $minduration = $dp_hr->{minduration} || 0; + + } + + # Whether because the user just got a new key created for + # them, or because they clicked too fast and we're reusing + # their old key, they have a key in $dpk. + + header(getData('head')) or return; + + slashDisplay('main', { + adnum => $adnum, + dpk => $dpk, + minduration => $minduration, + }); + + footer(); +} + +sub key_confirmed { + my($confcode) = @_; + # Pause to allow replication to catch up, so when + # the user gets back to the homepage, they will + # show up as having the daypass. + sleep 2; + setCookie('daypassconfcode', $confcode, '+24h'); + my $gSkin = getCurrentSkin(); + redirect($gSkin->{rootdir}); +} + +################################################################# +createEnvironment(); +main(); + +1; + Index: slashjp/plugins/Daypass/mysql_dump.sql diff -u /dev/null slashjp/plugins/Daypass/mysql_dump.sql:1.1 --- /dev/null Wed Jul 12 20:41:47 2006 +++ slashjp/plugins/Daypass/mysql_dump.sql Wed Jul 12 20:41:46 2006 @@ -0,0 +1,17 @@ +INSERT INTO hooks (param, class, subroutine) VALUES ('daypass_dooffer', 'Slash::Daypass', 'doOfferDaypass'); +INSERT INTO hooks (param, class, subroutine) VALUES ('daypass_getoffertext', 'Slash::Daypass', 'getOfferText'); + +INSERT INTO vars (name, value, description) VALUES ('daypass', '0', 'Activate daypass system?'); +INSERT INTO vars (name, value, description) VALUES ('daypass_adnum', '13', 'Which ad number to pass to getAd?'); +INSERT INTO vars (name, value, description) VALUES ('daypass_cache_expire', '60', 'How long is the cache of the daypass_available table stored?'); +INSERT INTO vars (name, value, description) VALUES ('daypass_offer', '0', 'Offer daypasses to logged-in non-subscriber users on the homepage?'); +INSERT INTO vars (name, value, description) VALUES ('daypass_offer_onlywhentmf', '0', 'Offer daypasses only when there is a story in The Mysterious Future?'); +INSERT INTO vars (name, value, description) VALUES ('daypass_offer_method', '0', 'How to determine whether a daypass is offered: 0=use daypass_available table, 1=check adpos text against regex'); +INSERT INTO vars (name, value, description) VALUES ('daypass_offer_method1_acl', '', 'ACL required to be offered a daypass (blank for none, i.e. all users eligible)'); +INSERT INTO vars (name, value, description) VALUES ('daypass_offer_method1_adpos', '31', 'If daypass_offer_method is 1, which ad position to check?'); +INSERT INTO vars (name, value, description) VALUES ('daypass_offer_method1_minduration', '10', 'Minimum time allowed before click'); +INSERT INTO vars (name, value, description) VALUES ('daypass_offer_method1_regex', '!placeholder', 'If daypass_offer_method is 1, what regex on that ad text tells us whether a daypass is available? A leading ! inverts logic (regex match means daypass not available)'); +INSERT INTO vars (name, value, description) VALUES ('daypass_offer_onlytologgedin', '0', 'If 1, offer a daypass only to logged-in users'); +INSERT INTO vars (name, value, description) VALUES ('daypass_seetmf', '0', 'Should users with daypasses be able to, like subscribers, see The Mysterious Future?'); +INSERT INTO vars (name, value, description) VALUES ('daypass_tz', 'PST', 'What timezone are daypasses considered to be in (this determines where "midnight" starts and ends the day)'); + Index: slashjp/plugins/Daypass/mysql_schema.sql diff -u /dev/null slashjp/plugins/Daypass/mysql_schema.sql:1.1 --- /dev/null Wed Jul 12 20:41:47 2006 +++ slashjp/plugins/Daypass/mysql_schema.sql Wed Jul 12 20:41:46 2006 @@ -0,0 +1,62 @@ +# Create a row in this table to indicate which daypass is available +# when. Times are in GMT. + +CREATE TABLE daypass_available ( + daid SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT, + adnum SMALLINT NOT NULL DEFAULT 0, + minduration SMALLINT NOT NULL DEFAULT 0, + starttime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + endtime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + aclreq VARCHAR(32) DEFAULT NULL, + PRIMARY KEY daid (daid) +) TYPE=InnoDB; + +# Creating rows in this table can mark certain skins or stories as +# requiring a daypass (or subscription) to read. Using this is +# optional (and currently there is no UI for admins to edit this +# data). To mark a story as daypass/subscription only, add a row +# of type='article', data='[story sid]'. To mark a skin such that +# only daypass and subscriber users can view its index page or any +# articles with that primaryskid, add a row of type='skin', +# data='[skid]'. To mark the entire site as requiring a daypass +# to read, set type='site', and data does not matter. +# The restrictions will be enforced from the starttime to the endtime, +# or if endtime is NULL, from the starttime on. +# If this table is empty, daypasses will be optional (and actually +# that's all that is supported in the code right now). + +CREATE TABLE daypass_needs ( + type ENUM('skin', 'site', 'article') NOT NULL DEFAULT 'skin', + data VARCHAR(255) NOT NULL, + starttime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + endtime DATETIME DEFAULT NULL +) TYPE=InnoDB; + +# Here is where daypass keys are temporarily stored, while users are +# looking at the daypass page(s). Once they have confirmed their key +# by completing the daypass page viewing, key_confirmed is set to +# non-NULL and a row for that user is created in daypass_users. +# Times are in GMT. + +CREATE TABLE daypass_keys ( + dpkid INT UNSIGNED NOT NULL AUTO_INCREMENT, + daypasskey CHAR(20) NOT NULL DEFAULT '', + daid SMALLINT UNSIGNED NOT NULL DEFAULT 0, + key_given DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + earliest_confirmable DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + key_confirmed DATETIME DEFAULT NULL, + PRIMARY KEY dpkid (dpkid), + UNIQUE daypasskey (daypasskey), + KEY key_given (key_given) +) TYPE=InnoDB; + +# Any user with a 'daypass_confcode' cookie in this table where the +# confcode >= NOW() is considered to have a daypass. It does not +# matter whether the user is logged-in. The time is in GMT. + +CREATE TABLE daypass_confcodes ( + confcode CHAR(20) NOT NULL DEFAULT '', + gooduntil DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY confcode (confcode) +) TYPE=InnoDB; +