450 lines
16 KiB
Perl
450 lines
16 KiB
Perl
# -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*-
|
|
#
|
|
# This file is MPL/GPL dual-licensed under the following terms:
|
|
#
|
|
# The contents of this file are subject to the Mozilla Public License
|
|
# Version 1.1 (the "License"); you may not use this file except in
|
|
# compliance with the License. You may obtain a copy of the License at
|
|
# http://www.mozilla.org/MPL/
|
|
#
|
|
# Software distributed under the License is distributed on an "AS IS"
|
|
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
|
|
# the License for the specific language governing rights and
|
|
# limitations under the License.
|
|
#
|
|
# The Original Code is PLIF 1.0.
|
|
# The Initial Developer of the Original Code is Ian Hickson.
|
|
#
|
|
# Alternatively, the contents of this file may be used under the terms
|
|
# of the GNU General Public License Version 2 or later (the "GPL"), in
|
|
# which case the provisions of the GPL are applicable instead of those
|
|
# above. If you wish to allow use of your version of this file only
|
|
# under the terms of the GPL and not to allow others to use your
|
|
# version of this file under the MPL, indicate your decision by
|
|
# deleting the provisions above and replace them with the notice and
|
|
# other provisions required by the GPL. If you do not delete the
|
|
# provisions above, a recipient may use your version of this file
|
|
# under either the MPL or the GPL.
|
|
|
|
package PLIF::Service::User;
|
|
use strict;
|
|
use vars qw(@ISA);
|
|
use PLIF::Service::Session;
|
|
@ISA = qw(PLIF::Service::Session);
|
|
1;
|
|
|
|
# This class implements an object and its associated factory service.
|
|
# Compare this with the UserFieldFactory class which implements a
|
|
# factory service only, and the various UserField descendant classes
|
|
# which implement Service Instances.
|
|
|
|
# XXX It would be interesting to implement crack detection (time since
|
|
# last incorrect login, number of login attempts performed with time
|
|
# since last incorrect login < a global delta, address changing
|
|
# timeout, etc).
|
|
|
|
sub provides {
|
|
my $class = shift;
|
|
my($service) = @_;
|
|
return ($service eq 'user.factory' or $class->SUPER::provides($service));
|
|
}
|
|
|
|
__DATA__
|
|
|
|
sub getUserByCredentials {
|
|
my $self = shift;
|
|
my($app, $username, $password) = @_;
|
|
my $object = $self->getUserByUsername($app, $username);
|
|
if (defined($object) and ($object->checkPassword($password))) {
|
|
return $object;
|
|
} else {
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
sub getUserByUsername {
|
|
my $self = shift;
|
|
my($app, $username) = @_;
|
|
my(@data) = $app->getService('dataSource.user')->getUserByUsername($app, $username);
|
|
if (@data) {
|
|
return $self->objectCreate($app, @data);
|
|
} else {
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
sub getUserByContactDetails {
|
|
my $self = shift;
|
|
my($app, $contactName, $address) = @_;
|
|
my(@data) = $app->getService('dataSource.user')->getUserByContactDetails($app, $contactName, $address);
|
|
if (@data) {
|
|
return $self->objectCreate($app, @data);
|
|
} else {
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
sub getUserByID {
|
|
my $self = shift;
|
|
my($app, $userID) = @_;
|
|
# do we already have that user?
|
|
my $user = $app->getObject("user.$userID");
|
|
if (defined($user)) {
|
|
# got a user created already, return that
|
|
return $user;
|
|
} else {
|
|
# that user isn't in our system yet, look it up
|
|
my(@data) = $app->getService('dataSource.user')->getUserByID($app, $userID);
|
|
if (@data) {
|
|
return $self->objectCreate($app, @data);
|
|
} else {
|
|
return undef;
|
|
}
|
|
}
|
|
}
|
|
|
|
sub getNewUser {
|
|
my $self = shift;
|
|
my($app, $password) = @_;
|
|
return $self->objectCreate($app, undef, 0, $password, '', {}, [], []);
|
|
}
|
|
|
|
sub objectProvides {
|
|
my $self = shift;
|
|
my($service) = @_;
|
|
return ($service eq 'user' or
|
|
$service eq 'user.'.($self->{userID}) or
|
|
$self->SUPER::objectProvides($service));
|
|
}
|
|
|
|
# We don't need this yet, so it's commented out as an optimisation.
|
|
# # Override the default constructor to check to see if the user already
|
|
# # has an object associated with them in the system, and if they do,
|
|
# # use that instead of creating a new one.
|
|
# sub objectCreate {
|
|
# my $class = shift;
|
|
# my($app, $userID, @data) @_;
|
|
# return $app->getObject("user.$userID") || $class->SUPER::objectCreate($app, $userID, @data);
|
|
# # more strictly:
|
|
# #
|
|
# # my $user = $app->getObject("user.$userID");
|
|
# # if (not defined($user)) {
|
|
# # $user = $class->SUPER::objectCreate($app, $userID, @data);
|
|
# # }
|
|
# # return $user;
|
|
# }
|
|
|
|
sub objectInit {
|
|
my $self = shift;
|
|
my($app, $userID, $mode, $password, $adminMessage, $fields, $groups, $rights) = @_;
|
|
$self->{'_DIRTY'} = {}; # make sure propertySet is happy
|
|
$self->SUPER::objectInit(@_);
|
|
$self->{userID} = $userID;
|
|
$self->{mode} = $mode; # 0=active, 1=disabled XXX need a way to make this extensible
|
|
$self->{password} = $password;
|
|
$self->{adminMessage} = $adminMessage;
|
|
$self->{fields} = {};
|
|
$self->{fieldsByID} = {};
|
|
# don't forget to update the 'hash' function if you add more properties/field whatever you want to call them
|
|
my $fieldFactory = $app->getService('user.fieldFactory');
|
|
foreach my $fieldID (keys(%$fields)) {
|
|
$self->insertField($fieldFactory->createFieldByID($app, $self, $fieldID, $fields->{$fieldID}));
|
|
}
|
|
# $groups is an array of arrays containing groupID, groupName, user level in group
|
|
my $groupsByID = {};
|
|
my $groupsByName = {};
|
|
foreach my $group (@$groups) {
|
|
$groupsByID->{$group->[0]} = {'name' => $group->[1], 'level' => $group->[2], }; # id => name, level
|
|
$groupsByName->{$group->[1]} = {'groupID' => $group->[0], 'level' => $group->[2], }; # name => id, level
|
|
}
|
|
$self->{groupsByID} = $groupsByID; # authoritative version
|
|
$self->{originalGroupsByID} = {%{$groupsByID}}; # a backup used to make a comparison when saving the groups
|
|
$self->{groupsByName} = $groupsByName; # helpful version for output purposes only
|
|
# rights
|
|
$self->{rights} = { map {$_ => 1} @$rights }; # map a list of strings into a hash for easy access
|
|
$self->{'_DIRTY'}->{'properties'} = not(defined($userID));
|
|
}
|
|
|
|
sub hasRight {
|
|
my $self = shift;
|
|
my($right) = @_;
|
|
return (defined($self->{rights}->{$right}) or $self->levelInGroup(1)); # group 1 is a magical group
|
|
}
|
|
|
|
sub hasField {
|
|
my $self = shift;
|
|
my($category, $name) = @_;
|
|
if (defined($self->{fields}->{$category})) {
|
|
return $self->{fields}->{$category}->{$name};
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
# returns a field *even if it does not exist yet*
|
|
# -- so if you want to add a field by name, you do:
|
|
# $user->getField('category', 'name')->data($myData);
|
|
sub getField {
|
|
my $self = shift;
|
|
my($category, $name) = @_;
|
|
my $field = $self->hasField($category, $name);
|
|
if (not defined($field)) {
|
|
$field = $self->insertField($self->{app}->getService('user.fieldFactory')->createFieldByName($self->{app}, $self, $category, $name));
|
|
}
|
|
return $field;
|
|
}
|
|
|
|
# returns a field *even if it does not exist yet*
|
|
# -- so if you want to add a field by ID, you do:
|
|
# $user->getField($fieldID)->data($myData);
|
|
sub getFieldByID {
|
|
my $self = shift;
|
|
my($ID) = @_;
|
|
my $field = $self->{fieldsByID}->{$ID};
|
|
if (not defined($field)) {
|
|
$field = $self->insertField($self->{app}->getService('user.fieldFactory')->createFieldByID($self->{app}, $self, $ID));
|
|
}
|
|
return $field;
|
|
}
|
|
|
|
sub getAddress {
|
|
my $self = shift;
|
|
my($protocol) = @_;
|
|
my $field = $self->hasField('contact', $protocol);
|
|
if (defined($field)) {
|
|
return $field->address;
|
|
} else {
|
|
return undef;
|
|
}
|
|
}
|
|
|
|
sub addFieldChange {
|
|
my $self = shift;
|
|
my($field, $newData, $password, $type) = @_;
|
|
$field->prepareChange($newData);
|
|
return $self->{app}->getService('dataSource.user')->setUserFieldChange($self->{app}, $self->{userID}, $field->{fieldID}, $newData, $password, $type);
|
|
}
|
|
|
|
sub performFieldChange {
|
|
my $self = shift;
|
|
my($changeID, $candidatePassword, $minTime) = @_;
|
|
my $dataSource = $self->{app}->getService('dataSource.user');
|
|
my($userID, $fieldID, $newData, $password, $createTime, $type) = $dataSource->getUserFieldChangeFromChangeID($self->{app}, $changeID);
|
|
# check for valid change
|
|
if ((not defined($userID)) or # invalid change ID
|
|
($userID != $self->{userID}) or # wrong change ID
|
|
(not $self->{app}->getService('service.password')->checkPassword($candidatePassword, $password)) or # wrong password
|
|
($createTime < $minTime)) { # expired change
|
|
return 0;
|
|
}
|
|
# perform the change
|
|
$self->getFieldByID($fieldID)->data = $newData;
|
|
# remove the change from the list of pending changes
|
|
if ($type == 1) { # XXX HARDCODED CONSTANT ALERT
|
|
# this is an override change
|
|
# remove all pending changes for this field (including this one)
|
|
$dataSource->removeUserFieldChangesByUserIDAndFieldID($self->{app}, $userID, $fieldID);
|
|
} else {
|
|
# this is a normal change
|
|
# remove just this change
|
|
$dataSource->removeUserFieldChangesByChangeID($self->{app}, $changeID);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
# a convenience method for either setting a user setting from a new
|
|
# value or getting the user's prefs
|
|
sub setting {
|
|
my $self = shift;
|
|
my($variable, $setting) = @_;
|
|
$self->assert(ref($variable) eq 'SCALAR', 1, 'Internal Error: User object was expecting a scalar ref for setting() but didn\'t get one');
|
|
if (defined($$variable)) {
|
|
$self->getField('settings', $setting)->data = $$variable;
|
|
} else {
|
|
my $field = $self->hasField('settings', $setting);
|
|
if (defined($field)) {
|
|
$$variable = $field->data;
|
|
}
|
|
}
|
|
}
|
|
|
|
sub hash {
|
|
my $self = shift;
|
|
my $result = $self->SUPER::hash();
|
|
$result->{'userID'} = $self->{userID},
|
|
$result->{'mode'} = $self->{mode},
|
|
$result->{'adminMessage'} = $self->{adminMessage},
|
|
$result->{'groupsByID'} = $self->{groupsByID};
|
|
$result->{'groupsByName'} = $self->{groupsByName};
|
|
$result->{'rights'} = [keys(%{$self->{rights}})];
|
|
if ($self->levelInGroup(1)) {
|
|
# has all rights
|
|
$result->{'right'} = {};
|
|
foreach my $right (@{$self->{app}->getService('dataSource.user')->getAllRights($self->{app})}) {
|
|
$result->{'right'}->{$right} = 1;
|
|
}
|
|
} else {
|
|
$result->{'right'} = $self->{rights};
|
|
}
|
|
$result->{'fields'} = {};
|
|
foreach my $field (values(%{$self->{fieldsByID}})) {
|
|
# XXX should we also pass the field metadata on? (e.g. typeData)
|
|
$result->{'fields'}->{$field->{fieldID}} = $field->hash; # (not an array btw: could have holes)
|
|
$result->{'fields'}->{$field->{category} . '.' . $field->{name}} = $field->hash;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
sub checkPassword {
|
|
my $self = shift;
|
|
my($password) = @_;
|
|
return $self->{app}->getService('service.passwords')->checkPassword($self->{password}, $password);
|
|
}
|
|
|
|
sub checkLogin {
|
|
my $self = shift;
|
|
return ($self->{mode} == 0);
|
|
}
|
|
|
|
sub joinGroup {
|
|
my $self = shift;
|
|
my($groupID, $level) = @_;
|
|
if ($level > 0) {
|
|
my $groupName = $self->{app}->getService('dataSource.user')->getGroupName($self->{app}, $groupID);
|
|
$self->{'groupsByID'}->{$groupID} = {'name' => $groupName, 'level' => $level, };
|
|
$self->{'groupsByName'}->{$groupName} = {'groupID' => $groupID, 'level' => $level, };
|
|
$self->invalidateRights();
|
|
$self->{'_DIRTY'}->{'groups'} = 1;
|
|
} else {
|
|
$self->leaveGroup($groupID, $level);
|
|
}
|
|
}
|
|
|
|
sub leaveGroup {
|
|
my $self = shift;
|
|
my($groupID) = @_;
|
|
if (defined($self->{'groupsByID'}->{$groupID})) {
|
|
delete($self->{'groupsByName'}->{$self->{'groupsByID'}->{$groupID}->{'name'}});
|
|
delete($self->{'groupsByID'}->{$groupID});
|
|
$self->invalidateRights();
|
|
$self->{'_DIRTY'}->{'groups'} = 1;
|
|
}
|
|
}
|
|
|
|
sub levelInGroup {
|
|
my $self = shift;
|
|
my($groupID) = @_;
|
|
if (defined($self->{'groupsByID'}->{$groupID})) {
|
|
return $self->{'groupsByID'}->{$groupID}->{'level'};
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
# internal routines
|
|
|
|
sub insertField {
|
|
my $self = shift;
|
|
my($field) = @_;
|
|
$self->assert(ref($field) and $field->provides('user.field'), 1, 'Tried to insert something that wasn\'t a field object into a user\'s field hash');
|
|
$self->{fields}->{$field->{category}}->{$field->{name}} = $field;
|
|
$self->{fieldsByID}->{$field->{fieldID}} = $field;
|
|
return $field;
|
|
}
|
|
|
|
sub invalidateRights {
|
|
my $self = shift;
|
|
my $rights = $self->{app}->getService('dataSource.user')->getRightsForGroups($self->{app}, keys(%{$self->{'groupsByID'}}));
|
|
$self->{rights} = { map {$_ => 1} @$rights }; # map a list of strings into a hash for easy access
|
|
# don't set a dirty flag, because rights are merely a convenient
|
|
# cached expansion of the rights data. Changing this externally
|
|
# makes no sense -- what rights one has is dependent on what
|
|
# groups one is in, and changing the rights won't magically change
|
|
# what groups you are in (how could it).
|
|
}
|
|
|
|
sub propertySet {
|
|
my $self = shift;
|
|
my($name, $value) = @_;
|
|
# check that we're not doing silly things like changing the user's ID
|
|
my $hadUndefinedID = (($name eq 'userID') and
|
|
($self->propertyExists($name)) and
|
|
(not defined($self->propertyGet($name))));
|
|
my $result = $self->SUPER::propertySet(@_);
|
|
if (($hadUndefinedID) and (defined($value))) {
|
|
# we've just aquired an ID, so propagate the change to all fields
|
|
foreach my $field (values(%{$self->{fieldsByID}})) {
|
|
$field->{userID} = $value;
|
|
}
|
|
# and mark the groups as dirty too
|
|
$self->{'_DIRTY'}->{'groups'} = 1;
|
|
}
|
|
$self->{'_DIRTY'}->{'properties'} = 1;
|
|
return $result;
|
|
}
|
|
|
|
sub propertyGet {
|
|
my $self = shift;
|
|
my($name) = @_;
|
|
if ($name eq 'groupsByID') {
|
|
return {%{$self->{'groupsByID'}}};
|
|
# Create new hash so that they can't edit ours. This ensures
|
|
# that they can't inadvertently bypass the DIRTY flagging by
|
|
# propertySet(), above. This does mean that internally we have
|
|
# to access $self->{'groupsByID'} instead of $self->{groupsByID}.
|
|
} else {
|
|
# we don't bother looking at $self->rights or
|
|
# $self->{groupsByName}, but any changes made to those won't be
|
|
# saved anyway.
|
|
return $self->SUPER::propertyGet(@_);
|
|
}
|
|
}
|
|
|
|
sub implyMethod {
|
|
my $self = shift;
|
|
my($name, @data) = @_;
|
|
if (@data == 1) {
|
|
return $self->propertySet($name, @data);
|
|
} elsif (@data == 0) {
|
|
return $self->propertyGet($name);
|
|
} else {
|
|
return $self->SUPER::implyMethod(@_);
|
|
}
|
|
}
|
|
|
|
sub DESTROY {
|
|
my $self = shift;
|
|
if ($self->{'_DIRTY'}->{'properties'}) {
|
|
$self->writeProperties();
|
|
}
|
|
if ($self->{'_DIRTY'}->{'groups'}) {
|
|
$self->writeGroups();
|
|
}
|
|
}
|
|
|
|
sub writeProperties {
|
|
my $self = shift;
|
|
$self->{userID} = $self->{app}->getService('dataSource.user')->setUser($self->{app}, $self->{userID}, $self->{mode},
|
|
$self->{password}, $self->{adminMessage},
|
|
$self->newFieldID, $self->{newFieldValue}, $self->{newFieldKey});
|
|
}
|
|
|
|
sub writeGroups {
|
|
my $self = shift;
|
|
# compare the group lists before and after and see which got added or changed and which got removed
|
|
my $dataSource = $self->{app}->getService('dataSource.user');
|
|
foreach my $group (keys(%{$self->{'groupsByID'}})) {
|
|
if ((not defined($self->{'originalGroupsByID'}->{$group})) or
|
|
($self->{'groupsByID'}->{$group}->{'level'} != $self->{'originalGroupsByID'}->{$group}->{'level'})) {
|
|
$dataSource->addUserGroup($self->{app}, $self->{userID}, $group, $self->{'groupsByID'}->{$group}->{'level'});
|
|
}
|
|
}
|
|
foreach my $group (keys(%{$self->{'originalGroupsByID'}})) {
|
|
if (not defined($self->{'groupsByID'}->{$group})) {
|
|
$dataSource->removeUserGroup($self->{app}, $self->{userID}, $group);
|
|
}
|
|
}
|
|
}
|
|
|
|
# fields write themselves out
|