Added new feature to validate groups against external databases

This commit is contained in:
Andy Grundman 2003-07-03 05:11:40 +00:00
parent 075c6d4e24
commit 2788250eeb
8 changed files with 725 additions and 475 deletions

View file

@ -1,2 +1,5 @@
insert into webguiVersion values ('5.5.0','upgrade',unix_timestamp()); insert into webguiVersion values ('5.5.0','upgrade',unix_timestamp());
insert into international (internationalId,languageId,namespace,message,lastUpdated) values (1004,1,'WebGUI','Cache external groups for how long?',1057208065);
insert into international (internationalId,languageId,namespace,message,lastUpdated) values (1005,1,'WebGUI','SQL Query',1057208065);
delete from international where languageId=1 and namespace='WebGUI' and internationalId=622;
insert into international (internationalId,languageId,namespace,message,lastUpdated) values (622,1,'WebGUI','See <i>Manage Group</i> for a description of grouping functions and the default groups.\r\n<p>\r\n\r\n<b>Group Name</b><br>\r\nA name for the group. It is best if the name is descriptive so you know what it is at a glance.\r\n<p>\r\n\r\n<b>Description</b><br>\r\nA longer description of the group so that other admins and content managers (or you if you forget) will know what the purpose of this group is.\r\n<p>\r\n\r\n<b>Expire Offset</b><br>\r\nThe amount of time that a user will belong to this group before s/he is expired (or removed) from it. This is very useful for membership sites where users have certain privileges for a specific period of time. \r\n<p>\r\n<b>NOTE:</b> This can be overridden on a per-user basis.\r\n<p>\r\n\r\n<b>Notify user about expiration?</b><br>\r\nSet this value to yes if you want WebGUI to contact the user when they are about to be expired from the group.\r\n<p>\r\n\r\n<b>Expire Notification Offset</b><br>\r\nThe difference in the number of days from the expiration to the notification. You may set this to any valid integer. For instance, set this to "0" if you wish the notification to be sent on the same day that the grouping expires. Set it to "-7" if you want the notification to go out 7 days <b>before</b> the grouping expires. Set it to "7" if you wish the notification to be sent 7 days after the expiration.\r\n<p>\r\n\r\n<b>Expire Notification Message</b><br>\r\nType the message you wish to be sent to the user telling them about the expiration.\r\n<p>\r\n\r\n<b>Delete Offset</b><br>\r\nThe difference in the number of days from the expiration to the grouping being deleted from the system. You may set this to any valid integer. For instance, set this to "0" if you wish the grouping to be deleted on the same day that the grouping expires. Set it to "-7" if you want the grouping to be deleted 7 days <b>before</b> the grouping expires. Set it to "7" if you wish the grouping to be deleted 7 days after the expiration.\r\n<p>\r\n\r\n<b>IP Address</b><br>\r\nSpecify an IP address or an IP mask to match. If the user\'s IP address matches, they\'ll automatically be included in this group. An IP mask is simply the IP address minus an octet or two. You may also specify multiple IP masks separated by semicolons.\r\n<p>\r\n<i>IP Mask Example:</i> 10.;192.168.;101.42.200.142\r\n<p>\r\n\r\n<b>Karma Threshold</b><br>\r\nIf you\'ve enabled Karma, then you\'ll be able to set this value. Karma Threshold is the amount of karma a user must have to be considered part of this group.\r\n<p>\r\n\r\n\r\n<b>Users can add themselves?</b><br>\r\nDo you wish to let users add themselves to this group? See the GroupAdd macro for more info.\r\n<p>\r\n\r\n<b>Users can remove themselves?</b><br>\r\nDo you wish to let users remove themselves from this group? See the GroupDelete macro for more info.\r\n<p>\r\n\r\n<i>The following options are recommended only for advanced WebGUI administrators.</i>\r\n<p>\r\n\r\n<b>Database Link</b><br>\r\nIf you\'d like to have this group validate users using an external database, choose the database link to use.\r\n<p>\r\n\r\n<b>SQL Query</b><br>\r\nMany organizations have external databases that map users to groups; for example an HR database might map Employee ID to Health Care Plan. To validate users against an external database, you need to construct a SQL statement that will return 1 if a user is in the group. Make sure to begin your statement with "select 1". You may use macros in this query to access data in a user\'s profile, such as Employee ID. Here is an example that checks a user against a fictional HR database. This assumes you have created an additional profile field called employeeId.<br>\r\n<br>\r\nselect 1 from employees, health_plans, empl_plan_map<br>\r\nwhere employees.employee_id = ^User("employeeId");<br>\r\nand health_plans.plan_name = \'HMO 1\'<br>\r\nand employees.employee_id = empl_plan_map.employee_id<br>\r\nand health_plans.health_plan_id = empl_plan_mp.health_plan_id<br>\r\n<br>\r\nThis group could then be named "Employees in HMO 1", and would allow you to restrict any page or wobject to only those users who are part of this health plan in the external database.\r\n<p>\r\n\r\n<b>Cache external groups for how long?</b><br>\r\nLarge sites using external group data will be making many calls to the external database. To help reduce the load, you may select how long you\'d like to cache the results of the external database query within the WebGUI database. More advanced background caching may be included in a future version of WebGUI.',1053779630);

View file

@ -35,6 +35,10 @@ This package contains utility methods for WebGUI's database link system.
%databaseLink = WebGUI::DatabaseLink::get($databaseLinkId); %databaseLink = WebGUI::DatabaseLink::get($databaseLinkId);
%using = WebGUI::Databaselink::whatIsUsing($databaseLinkId); %using = WebGUI::Databaselink::whatIsUsing($databaseLinkId);
$dbLink = WebGUI::DatabaseLink->new($databaseLinkId);
$dbh = $dbLink->dbh;
$dbLink->disconnect;
=head1 METHODS =head1 METHODS
These subroutines are available from this package: These subroutines are available from this package:
@ -75,7 +79,8 @@ sub get {
#------------------------------------------------------------------- #-------------------------------------------------------------------
=head2 whatIsUsing ( databaseLinkId ) =head2 whatIsUsing ( databaseLinkId )
Returns an array of hashrefs containing wobjects which use a database link. Returns an array of hashrefs containing items which use a database link. This method will
need to be updated any time a new item starts using Database Links.
=over =over
@ -88,6 +93,7 @@ A valid databaseLinkId
=cut =cut
sub whatIsUsing { sub whatIsUsing {
# get list of SQLReports
my $sql = 'select wobject.wobjectId, wobject.title, page.menuTitle, page.urlizedTitle from wobject, SQLReport, page '. my $sql = 'select wobject.wobjectId, wobject.title, page.menuTitle, page.urlizedTitle from wobject, SQLReport, page '.
'where SQLReport.databaseLinkId = '.$_[0].' and SQLReport.wobjectId = wobject.wobjectId '. 'where SQLReport.databaseLinkId = '.$_[0].' and SQLReport.wobjectId = wobject.wobjectId '.
'and wobject.pageId = page.pageId'; 'and wobject.pageId = page.pageId';
@ -97,8 +103,98 @@ sub whatIsUsing {
push @using, $data; push @using, $data;
} }
$sth->finish; $sth->finish;
# get list of groups
$sql = 'select groupId, groupName from groups where databaseLinkId = '.$_[0];
$sth = WebGUI::SQL->read($sql);
while (my $data = $sth->hashRef()) {
push @using, $data;
}
$sth->finish;
return @using; return @using;
} }
#-------------------------------------------------------------------
=head2 disconnect ( )
Disconnect cleanly from the current databaseLink.
=cut
sub disconnect {
my ($class, $value);
$class = shift;
$value = shift;
if (defined $class->{_dbh}) {
$class->{_dbh}->disconnect() unless ($class->{_databaseLink}{DSN} eq $session{config}{dsn});
}
}
#-------------------------------------------------------------------
=head2 dbh ( )
Return a DBI handle for the current databaseLink, connecting if necessary.
=cut
sub dbh {
my ($class, $value);
my ($dsn, $username, $identifier);
$class = shift;
$value = shift;
if (defined $class->{_dbh}) {
return $class->{_dbh};
}
$dsn = $class->{_databaseLink}{DSN};
$username = $class->{_databaseLink}{username};
$identifier = $class->{_databaseLink}{identifier};
if ($dsn eq $session{config}{dsn}) {
$class->{_dbh} = $session{dbh};
return $session{dbh};
} elsif ($dsn =~ /\DBI\:\w+\:\w+/i) {
eval{
$class->{_dbh} = DBI->connect($dsn,$username,$identifier);
};
if ($@) {
WebGUI::ErrorHandler::warn("DatabaseLink [".$_[0]."] ".$@);
} else {
return $class->{_dbh};
}
} else {
WebGUI::ErrorHandler::warn("DatabaseLink [".$_[0]."] The DSN specified is of an improper format.");
}
return undef;
}
#-------------------------------------------------------------------
=head2 new ( databaseLinkId )
Constructor.
=over
=item databaseLinkId
The databaseLinkId of the databaseLink you're creating an object reference for.
=back
=cut
sub new {
my ($class, $databaseLinkId, %databaseLink);
tie %databaseLink, 'Tie::CPHash';
$class = shift;
$databaseLinkId = shift;
unless ($databaseLinkId eq "") {
%databaseLink = WebGUI::SQL->quickHash("select * from databaseLink where databaseLinkId='$databaseLinkId'");
}
bless {_databaseLinkId => $databaseLinkId, _databaseLink => \%databaseLink }, $class;
}
1; 1;

View file

@ -535,6 +535,7 @@ sub new {
$group{expireNotifyOffset} = -14; $group{expireNotifyOffset} = -14;
$group{deleteOffset} = 14; $group{deleteOffset} = 14;
$group{expireNotify} = 0; $group{expireNotify} = 0;
$group{dbCacheTimeout} = 3600;
} else { } else {
%group = WebGUI::SQL->quickHash("select * from groups where groupId='$groupId'"); %group = WebGUI::SQL->quickHash("select * from groups where groupId='$groupId'");
} }
@ -569,6 +570,90 @@ sub scratchFilter {
return $class->{_group}{"scratchFilter"}; return $class->{_group}{"scratchFilter"};
} }
#-------------------------------------------------------------------
=head2 dbQuery ( [ value ] )
Returns the dbQuery for this group.
=over
=item value
If specified, the dbQuery is set to this value.
=back
=cut
sub dbQuery {
my ($class, $value);
$class = shift;
$value = shift;
if (defined $value) {
$class->{_group}{"dbQuery"} = $value;
WebGUI::SQL->write("update groups set dbQuery=".quote($value).",
lastUpdated=".time()." where groupId=$class->{_groupId}");
}
return $class->{_group}{"dbQuery"};
}
#-------------------------------------------------------------------
=head2 databaseLinkId ( [ value ] )
Returns the databaseLinkId for this group.
=over
=item value
If specified, the databaseLinkId is set to this value.
=back
=cut
sub databaseLinkId {
my ($class, $value);
$class = shift;
$value = shift;
if (defined $value) {
$class->{_group}{"databaseLinkId"} = $value;
WebGUI::SQL->write("update groups set databaseLinkId=".quote($value).",
lastUpdated=".time()." where groupId=$class->{_groupId}");
}
return $class->{_group}{"databaseLinkId"};
}
#-------------------------------------------------------------------
=head2 dbCacheTimeout ( [ value ] )
Returns the dbCacheTimeout for this group.
=over
=item value
If specified, the dbCacheTimeout is set to this value.
=back
=cut
sub dbCacheTimeout {
my ($class, $value);
$class = shift;
$value = shift;
if (defined $value) {
$class->{_group}{"dbCacheTimeout"} = $value;
WebGUI::SQL->write("update groups set dbCacheTimeout=".quote($value).",
lastUpdated=".time()." where groupId=$class->{_groupId}");
}
return $class->{_group}{"dbCacheTimeout"};
}
1; 1;

View file

@ -60,8 +60,12 @@ sub www_deleteDatabaseLink {
$output .= '<h1>'.WebGUI::International::get(987).'</h1>'; $output .= '<h1>'.WebGUI::International::get(987).'</h1>';
$output .= WebGUI::International::get(988).'<p>'; $output .= WebGUI::International::get(988).'<p>';
foreach my $using (WebGUI::DatabaseLink::whatIsUsing($session{form}{dlid})) { foreach my $using (WebGUI::DatabaseLink::whatIsUsing($session{form}{dlid})) {
$output .= '<li>'.WebGUI::International::get(1,'SQL Report').' <a href="'.WebGUI::URL::page('func=edit&wid='.$using->{wobjectId},$using->{urlizedTitle}).'">' if ($using->{title}) {
.$using->{title}.'</a> '.WebGUI::International::get(989).' <a href="'.WebGUI::URL::gateway($using->{urlizedTitle}).'">'.$using->{menuTitle}.'</a>.</li>'; $output .= '<li>'.WebGUI::International::get(1,'SQLReport').' <a href="'.WebGUI::URL::page('func=edit&wid='.$using->{wobjectId},$using->{urlizedTitle}).'">'
.$using->{title}.'</a> '.WebGUI::International::get(989).' <a href="'.WebGUI::URL::gateway($using->{urlizedTitle}).'">'.$using->{menuTitle}.'</a>.</li>';
} else {
$output .= '<li>'.'Group'.' <a href="'.WebGUI::URL::page('op=editGroup&gid='.$using->{groupId}).'">'.$using->{groupName}.'</a>.</li>';
}
} }
$output .= '<p><div align="center"><a href="'. $output .= '<p><div align="center"><a href="'.
WebGUI::URL::page('op=deleteDatabaseLinkConfirm&dlid='.$session{form}{dlid}) WebGUI::URL::page('op=deleteDatabaseLinkConfirm&dlid='.$session{form}{dlid})

View file

@ -13,6 +13,7 @@ package WebGUI::Operation::Group;
use Exporter; use Exporter;
use strict; use strict;
use Tie::CPHash; use Tie::CPHash;
use WebGUI::DatabaseLink;
use WebGUI::DateTime; use WebGUI::DateTime;
use WebGUI::Group; use WebGUI::Group;
use WebGUI::Grouping; use WebGUI::Grouping;
@ -216,6 +217,22 @@ sub www_editGroup {
-value=>$g->autoDelete, -value=>$g->autoDelete,
-label=>WebGUI::International::get(975) -label=>WebGUI::International::get(975)
); );
$f->selectList(
-name=>"databaseLinkId",
-options=>{
"0"=>WebGUI::International::get(19,'SQLReport'),
WebGUI::DatabaseLink::getHash(),
},
-label=>WebGUI::International::get(20,'SQLReport'),
-value=>[$g->databaseLinkId],
-subtext=>(WebGUI::Privilege::isInGroup(3)) ? '<a href="'.WebGUI::URL::page("op=listDatabaseLinks").'">'.WebGUI::International::get(981).'</a>' : ""
);
$f->textarea(
-name=>"dbQuery",
-value=>$g->dbQuery,
-label=>WebGUI::International::get(1005)
);
$f->interval("dbCacheTimeout",WebGUI::International::get(1004), WebGUI::DateTime::secondsToInterval($g->dbCacheTimeout));
$f->submit; $f->submit;
$output .= $f->print; $output .= $f->print;
return _submenu($output); return _submenu($output);
@ -237,6 +254,9 @@ sub www_editGroupSave {
$g->deleteOffset($session{form}{deleteOffset}); $g->deleteOffset($session{form}{deleteOffset});
$g->autoAdd(WebGUI::FormProcessor::yesNo("autoAdd")); $g->autoAdd(WebGUI::FormProcessor::yesNo("autoAdd"));
$g->autoDelete(WebGUI::FormProcessor::yesNo("autoDelete")); $g->autoDelete(WebGUI::FormProcessor::yesNo("autoDelete"));
$g->databaseLinkId($session{form}{databaseLinkId});
$g->dbQuery($session{form}{dbQuery});
$g->dbCacheTimeout(WebGUI::FormProcessor::interval("dbCacheTimeout"));
return www_listGroups(); return www_listGroups();
} }

View file

@ -16,10 +16,12 @@ package WebGUI::Privilege;
use strict; use strict;
use Tie::CPHash; use Tie::CPHash;
use WebGUI::DatabaseLink;
use WebGUI::DateTime; use WebGUI::DateTime;
use WebGUI::Group; use WebGUI::Group;
use WebGUI::Grouping; use WebGUI::Grouping;
use WebGUI::International; use WebGUI::International;
use WebGUI::Macro;
use WebGUI::Operation::Account (); use WebGUI::Operation::Account ();
use WebGUI::Session; use WebGUI::Session;
use WebGUI::SQL; use WebGUI::SQL;
@ -302,7 +304,7 @@ sub isInGroup {
} }
### Get data for auxillary checks. ### Get data for auxillary checks.
tie %group, 'Tie::CPHash'; tie %group, 'Tie::CPHash';
%group = WebGUI::SQL->quickHash("select karmaThreshold,ipFilter,scratchFilter from groups where groupId='$gid'"); %group = WebGUI::SQL->quickHash("select karmaThreshold,ipFilter,scratchFilter,databaseLinkId,dbQuery,dbCacheTimeout from groups where groupId='$gid'");
### Check IP Address ### Check IP Address
if ($group{ipFilter} ne "") { if ($group{ipFilter} ne "") {
$group{ipFilter} =~ s/\t//g; $group{ipFilter} =~ s/\t//g;
@ -346,6 +348,42 @@ sub isInGroup {
return 1; return 1;
} }
} }
### Check external database
if ($group{dbQuery} ne "" && $group{databaseLinkId}) {
# skip if not logged in and query contains a User macro
unless ($group{dbQuery} =~ /\^User/i && $uid == 1) {
my $dbLink = WebGUI::DatabaseLink->new($group{databaseLinkId});
my $dbh = $dbLink->dbh;
if (defined $dbh) {
if ($group{dbQuery} =~ /select 1/i) {
$group{dbQuery} = WebGUI::Macro::process($group{dbQuery});
my $sth = WebGUI::SQL->unconditionalRead($group{dbQuery},$dbh);
unless ($sth->errorCode < 1) {
WebGUI::ErrorHandler::warn("There was a problem with the database query for group ID $gid.");
} else {
my ($result) = $sth->array;
if ($result == 1) {
$session{isInGroup}{$gid}{$uid} = 1;
if ($group{dbCacheTimeout} > 0) {
WebGUI::Grouping::deleteUsersFromGroups([$uid],[$gid]);
WebGUI::Grouping::addUsersToGroups([$uid],[$gid],$group{dbCacheTimeout});
}
} else {
$session{isInGroup}{$gid}{$uid} = 0;
WebGUI::Grouping::deleteUsersFromGroups([$uid],[$gid]) if ($group{dbCacheTimeout} > 0);
}
}
$sth->finish;
} else {
WebGUI::ErrorHandler::warn("Database query for group ID $gid must use 'select 1'");
}
$dbLink->disconnect;
return 1 if ($session{isInGroup}{$gid}{$uid});
}
}
}
### Check for groups of groups. ### Check for groups of groups.
$groups = WebGUI::Grouping::getGroupsInGroup($gid,1); $groups = WebGUI::Grouping::getGroupsInGroup($gid,1);
foreach (@{$groups}) { foreach (@{$groups}) {

View file

@ -148,7 +148,7 @@ sub www_edit {
#------------------------------------------------------------------- #-------------------------------------------------------------------
sub www_view { sub www_view {
my ($dsn, $username, $identifier, $query, @row, $i, $rownum, $p, $ouch, $output, $sth, $dbh, @result, @template, $temp, $col, $errorMessage, $url); my ($dsn, $username, $identifier, $dbLink, $query, @row, $i, $rownum, $p, $ouch, $output, $sth, $dbh, @result, @template, $temp, $col, $errorMessage, $url);
if ($_[0]->get("preprocessMacros")) { if ($_[0]->get("preprocessMacros")) {
$query = WebGUI::Macro::process($_[0]->get("dbQuery")); $query = WebGUI::Macro::process($_[0]->get("dbQuery"));
} else { } else {
@ -161,29 +161,24 @@ sub www_view {
$output .= $_[0]->description; $output .= $_[0]->description;
$output .= WebGUI::International::get(17,$_[0]->get("namespace"))." ".$query."<p>" if ($_[0]->get("debugMode")); $output .= WebGUI::International::get(17,$_[0]->get("namespace"))." ".$query."<p>" if ($_[0]->get("debugMode"));
# pull database link info if selected # connect to external database if used
if ($_[0]->get("databaseLinkId")) { if ($_[0]->get("databaseLinkId")) {
my %databaseLink = WebGUI::DatabaseLink::get($_[0]->get("databaseLinkId")); $dbLink = WebGUI::DatabaseLink->new($_[0]->get("databaseLinkId"));
# failsafe check in case the link gets deleted $dbh = $dbLink->dbh;
if ($databaseLink{DSN}) { } else {
$dsn = $databaseLink{DSN}; if ($dsn eq $session{config}{dsn}) {
$username = $databaseLink{username}; $dbh = $session{dbh};
$identifier = $databaseLink{identifier}; } elsif ($dsn =~ /\DBI\:\w+\:\w+/i) {
eval{$dbh = DBI->connect($dsn,$username,$identifier)};
if ($@) {
WebGUI::ErrorHandler::warn("SQL Report [".$_[0]->get("wobjectId")."] ".$@);
undef $dbh;
}
} else {
$output .= WebGUI::International::get(9,$_[0]->get("namespace")).'<p>' if ($_[0]->get("debugMode"));
WebGUI::ErrorHandler::warn("SQLReport [".$_[0]->get("wobjectId")."] The DSN specified is of an improper format.");
} }
} }
if ($dsn eq $session{config}{dsn}) {
$dbh = $session{dbh};
} elsif ($dsn =~ /\DBI\:\w+\:\w+/) {
eval{$dbh = DBI->connect($dsn,$username,$identifier)};
if ($@) {
WebGUI::ErrorHandler::warn("SQL Report [".$_[0]->get("wobjectId")."] ".$@);
undef $dbh;
}
} else {
$output .= WebGUI::International::get(9,$_[0]->get("namespace")).'<p>' if ($_[0]->get("debugMode"));
WebGUI::ErrorHandler::warn("SQLReport [".$_[0]->get("wobjectId")."] The DSN specified is of an improper format.");
}
if (defined $dbh) { if (defined $dbh) {
if ($query =~ /^select/i || $query =~ /^show/i || $query =~ /^describe/i) { if ($query =~ /^select/i || $query =~ /^show/i || $query =~ /^describe/i) {
$sth = WebGUI::SQL->unconditionalRead($query,$dbh); $sth = WebGUI::SQL->unconditionalRead($query,$dbh);
@ -242,7 +237,11 @@ sub www_view {
$output .= WebGUI::International::get(10,$_[0]->get("namespace")).'<p>' if ($_[0]->get("debugMode")); $output .= WebGUI::International::get(10,$_[0]->get("namespace")).'<p>' if ($_[0]->get("debugMode"));
WebGUI::ErrorHandler::warn("SQLReport [".$_[0]->get("wobjectId")."] The SQL query is improperly formatted."); WebGUI::ErrorHandler::warn("SQLReport [".$_[0]->get("wobjectId")."] The SQL query is improperly formatted.");
} }
$dbh->disconnect() unless ($dsn eq $session{config}{dsn}); if ($dbLink) {
$dbLink->disconnect;
} else {
$dbh->disconnect() unless ($dsn eq $session{config}{dsn});
}
} else { } else {
$output .= WebGUI::International::get(12,$_[0]->get("namespace")).'<p>' if ($_[0]->get("debugMode")); $output .= WebGUI::International::get(12,$_[0]->get("namespace")).'<p>' if ($_[0]->get("debugMode"));
WebGUI::ErrorHandler::warn("SQLReport [".$_[0]->get("wobjectId")."] Could not connect to database."); WebGUI::ErrorHandler::warn("SQLReport [".$_[0]->get("wobjectId")."] Could not connect to database.");

View file

@ -19,10 +19,15 @@ use WebGUI::SQL;
sub process { sub process {
my @date = WebGUI::DateTime::localtime(); my @date = WebGUI::DateTime::localtime();
if ($date[4] == 3) { # only occurs at 3am on the day in question. if ($date[4] == 3) { # only occurs at 3am on the day in question.
my $sth = WebGUI::SQL->read("select groupId,deleteOffset from groups"); my $sth = WebGUI::SQL->read("select groupId,deleteOffset,dbCacheTimeout from groups");
while (my $data = $sth->hashRef) { while (my $data = $sth->hashRef) {
WebGUI::SQL->write("delete from groupings where groupId=$data->{groupId} and expireDate < " if ($data->{dbCacheTimeout} > 0) {
# there is no need to wait deleteOffset days for expired external group cache data
WebGUI::SQL->write("delete from groupings where groupId=$data->{groupId} and expireDate < ".time());
} else {
WebGUI::SQL->write("delete from groupings where groupId=$data->{groupId} and expireDate < "
.(time()-(86400*$data->{deleteOffset}))); .(time()-(86400*$data->{deleteOffset})));
}
} }
$sth->finish; $sth->finish;
} }