← Index
NYTProf Performance Profile   « line view »
For ./view
  Run on Fri Jul 31 18:42:36 2015
Reported on Fri Jul 31 18:48:15 2015

Filename/var/www/foswikidev/core/lib/Foswiki/Users/TopicUserMapping.pm
StatementsExecuted 14146 statements in 27.3ms
Subroutines
Calls P F Exclusive
Time
Inclusive
Time
Subroutine
1069119.76ms21.1msFoswiki::Users::TopicUserMapping::::_cacheUserFoswiki::Users::TopicUserMapping::_cacheUser
5218.96ms31.9msFoswiki::Users::TopicUserMapping::::_loadMappingFoswiki::Users::TopicUserMapping::_loadMapping
1066327.21ms8.81msFoswiki::Users::TopicUserMapping::::login2cUIDFoswiki::Users::TopicUserMapping::login2cUID
1111.09ms1.10msFoswiki::Users::TopicUserMapping::::finishFoswiki::Users::TopicUserMapping::finish
111746µs832µsFoswiki::Users::TopicUserMapping::::BEGIN@35Foswiki::Users::TopicUserMapping::BEGIN@35
111614µs716µsFoswiki::Users::TopicUserMapping::::newFoswiki::Users::TopicUserMapping::new
6211165µs165µsFoswiki::Users::TopicUserMapping::::isGroupFoswiki::Users::TopicUserMapping::isGroup
43166µs31.9msFoswiki::Users::TopicUserMapping::::_userReallyExistsFoswiki::Users::TopicUserMapping::_userReallyExists
11115µs38µsFoswiki::Users::TopicUserMapping::::getLoginNameFoswiki::Users::TopicUserMapping::getLoginName
11115µs19µsFoswiki::Users::TopicUserMapping::::BEGIN@204Foswiki::Users::TopicUserMapping::BEGIN@204
31115µs31.9msFoswiki::Users::TopicUserMapping::::handlesUserFoswiki::Users::TopicUserMapping::handlesUser
11111µs14µsFoswiki::Users::TopicUserMapping::::BEGIN@209Foswiki::Users::TopicUserMapping::BEGIN@209
11110µs15µsFoswiki::Users::TopicUserMapping::::BEGIN@32Foswiki::Users::TopicUserMapping::BEGIN@32
11110µs10µsFoswiki::Users::TopicUserMapping::::BEGIN@28Foswiki::Users::TopicUserMapping::BEGIN@28
1119µs35µsFoswiki::Users::TopicUserMapping::::BEGIN@33Foswiki::Users::TopicUserMapping::BEGIN@33
1119µs10µsFoswiki::Users::TopicUserMapping::::getWikiNameFoswiki::Users::TopicUserMapping::getWikiName
1119µs108µsFoswiki::Users::TopicUserMapping::::BEGIN@34Foswiki::Users::TopicUserMapping::BEGIN@34
1118µs22µsFoswiki::Users::TopicUserMapping::::BEGIN@31Foswiki::Users::TopicUserMapping::BEGIN@31
1114µs4µsFoswiki::Users::TopicUserMapping::::BEGIN@36Foswiki::Users::TopicUserMapping::BEGIN@36
111800ns800nsFoswiki::Users::TopicUserMapping::::supportsRegistrationFoswiki::Users::TopicUserMapping::supportsRegistration
0000s0sFoswiki::Users::TopicUserMapping::::__ANON__[:467]Foswiki::Users::TopicUserMapping::__ANON__[:467]
0000s0sFoswiki::Users::TopicUserMapping::::__ANON__[:475]Foswiki::Users::TopicUserMapping::__ANON__[:475]
0000s0sFoswiki::Users::TopicUserMapping::::__ANON__[:631]Foswiki::Users::TopicUserMapping::__ANON__[:631]
0000s0sFoswiki::Users::TopicUserMapping::::__ANON__[:782]Foswiki::Users::TopicUserMapping::__ANON__[:782]
0000s0sFoswiki::Users::TopicUserMapping::::_clearGroupCacheFoswiki::Users::TopicUserMapping::_clearGroupCache
0000s0sFoswiki::Users::TopicUserMapping::::_collateGroupsFoswiki::Users::TopicUserMapping::_collateGroups
0000s0sFoswiki::Users::TopicUserMapping::::_expandUserListFoswiki::Users::TopicUserMapping::_expandUserList
0000s0sFoswiki::Users::TopicUserMapping::::_getListOfGroupsFoswiki::Users::TopicUserMapping::_getListOfGroups
0000s0sFoswiki::Users::TopicUserMapping::::_maintainUsersTopicFoswiki::Users::TopicUserMapping::_maintainUsersTopic
0000s0sFoswiki::Users::TopicUserMapping::::_writeGroupTopicFoswiki::Users::TopicUserMapping::_writeGroupTopic
0000s0sFoswiki::Users::TopicUserMapping::::addUserFoswiki::Users::TopicUserMapping::addUser
0000s0sFoswiki::Users::TopicUserMapping::::addUserToGroupFoswiki::Users::TopicUserMapping::addUserToGroup
0000s0sFoswiki::Users::TopicUserMapping::::checkPasswordFoswiki::Users::TopicUserMapping::checkPassword
0000s0sFoswiki::Users::TopicUserMapping::::eachGroupFoswiki::Users::TopicUserMapping::eachGroup
0000s0sFoswiki::Users::TopicUserMapping::::eachGroupMemberFoswiki::Users::TopicUserMapping::eachGroupMember
0000s0sFoswiki::Users::TopicUserMapping::::eachMembershipFoswiki::Users::TopicUserMapping::eachMembership
0000s0sFoswiki::Users::TopicUserMapping::::eachUserFoswiki::Users::TopicUserMapping::eachUser
0000s0sFoswiki::Users::TopicUserMapping::::findUserByEmailFoswiki::Users::TopicUserMapping::findUserByEmail
0000s0sFoswiki::Users::TopicUserMapping::::findUserByWikiNameFoswiki::Users::TopicUserMapping::findUserByWikiName
0000s0sFoswiki::Users::TopicUserMapping::::getEmailsFoswiki::Users::TopicUserMapping::getEmails
0000s0sFoswiki::Users::TopicUserMapping::::groupAllowsChangeFoswiki::Users::TopicUserMapping::groupAllowsChange
0000s0sFoswiki::Users::TopicUserMapping::::groupAllowsViewFoswiki::Users::TopicUserMapping::groupAllowsView
0000s0sFoswiki::Users::TopicUserMapping::::isAdminFoswiki::Users::TopicUserMapping::isAdmin
0000s0sFoswiki::Users::TopicUserMapping::::mapper_getEmailsFoswiki::Users::TopicUserMapping::mapper_getEmails
0000s0sFoswiki::Users::TopicUserMapping::::mapper_setEmailsFoswiki::Users::TopicUserMapping::mapper_setEmails
0000s0sFoswiki::Users::TopicUserMapping::::passwordErrorFoswiki::Users::TopicUserMapping::passwordError
0000s0sFoswiki::Users::TopicUserMapping::::removeUserFoswiki::Users::TopicUserMapping::removeUser
0000s0sFoswiki::Users::TopicUserMapping::::removeUserFromGroupFoswiki::Users::TopicUserMapping::removeUserFromGroup
0000s0sFoswiki::Users::TopicUserMapping::::setEmailsFoswiki::Users::TopicUserMapping::setEmails
0000s0sFoswiki::Users::TopicUserMapping::::setPasswordFoswiki::Users::TopicUserMapping::setPassword
0000s0sFoswiki::Users::TopicUserMapping::::userExistsFoswiki::Users::TopicUserMapping::userExists
0000s0sFoswiki::Users::TopicUserMapping::::validateRegistrationFieldFoswiki::Users::TopicUserMapping::validateRegistrationField
Call graph for these subroutines as a Graphviz dot language file.
Line State
ments
Time
on line
Calls Time
in subs
Code
1# See bottom of file for license and copyright information
2
3=begin TML
4
5---+ package Foswiki::Users::TopicUserMapping @isa Foswiki::UserMapping');
6
7use
8
9The User mapping is the process by which Foswiki maps from a username (a login name)
10to a wikiname and back. It is also where groups are defined.
11
12By default Foswiki maintains user topics and group topics in the %MAINWEB% that
13define users and group. These topics are
14 * !WikiUsers - stores a mapping from usernames to Wiki names
15 * !WikiName - for each user, stores info about the user
16 * !GroupNameGroup - for each group, a topic ending with "Group" stores a list of users who are part of that group.
17
18Many sites will want to override this behaviour, for example to get users and groups from a corporate database.
19
20This class implements the basic Foswiki behaviour using topics to store users,
21but is also designed to be subclassed so that other services can be used.
22
23Subclasses should be named 'XxxxUserMapping' so that configure can find them.
24
25=cut
26
27package Foswiki::Users::TopicUserMapping;
28243µs110µs
# spent 10µs within Foswiki::Users::TopicUserMapping::BEGIN@28 which was called: # once (10µs+0s) by Foswiki::Users::new at line 28
use Foswiki::UserMapping ();
# spent 10µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@28
2917µsour @ISA = ('Foswiki::UserMapping');
30
31229µs235µs
# spent 22µs (8+13) within Foswiki::Users::TopicUserMapping::BEGIN@31 which was called: # once (8µs+13µs) by Foswiki::Users::new at line 31
use strict;
# spent 22µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@31 # spent 13µs making 1 call to strict::import
32224µs219µs
# spent 15µs (10+5) within Foswiki::Users::TopicUserMapping::BEGIN@32 which was called: # once (10µs+5µs) by Foswiki::Users::new at line 32
use warnings;
# spent 15µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@32 # spent 5µs making 1 call to warnings::import
33228µs261µs
# spent 35µs (9+26) within Foswiki::Users::TopicUserMapping::BEGIN@33 which was called: # once (9µs+26µs) by Foswiki::Users::new at line 33
use Assert;
# spent 35µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@33 # spent 26µs making 1 call to Exporter::import
34228µs2208µs
# spent 108µs (9+100) within Foswiki::Users::TopicUserMapping::BEGIN@34 which was called: # once (9µs+100µs) by Foswiki::Users::new at line 34
use Error qw( :try );
# spent 108µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@34 # spent 100µs making 1 call to Error::import
35296µs1832µs
# spent 832µs (746+85) within Foswiki::Users::TopicUserMapping::BEGIN@35 which was called: # once (746µs+85µs) by Foswiki::Users::new at line 35
use Foswiki::ListIterator ();
# spent 832µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@35
362440µs14µs
# spent 4µs within Foswiki::Users::TopicUserMapping::BEGIN@36 which was called: # once (4µs+0s) by Foswiki::Users::new at line 36
use Foswiki::Func ();
# spent 4µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@36
37
38#use Monitor;
39#Monitor::MonitorMethod('Foswiki::Users::TopicUserMapping');
40
41=begin TML
42
43---++ ClassMethod new ($session, $impl)
44
45Constructs a new user mapping handler of this type, referring to $session
46for any required Foswiki services.
47
48=cut
49
50# The null mapping name is reserved for Foswiki for backward-compatibility.
51# We declare this as a global variable so we can override it during testing.
521300nsour $FOSWIKI_USER_MAPPING_ID = '';
53
54#our $FOSWIKI_USER_MAPPING_ID = 'TestMapping_';
55
56
# spent 716µs (614+101) within Foswiki::Users::TopicUserMapping::new which was called: # once (614µs+101µs) by Foswiki::Users::new at line 105 of /var/www/foswikidev/core/lib/Foswiki/Users.pm
sub new {
571900ns my ( $class, $session ) = @_;
58
59116µs19µs my $this = $class->SUPER::new( $session, $FOSWIKI_USER_MAPPING_ID );
# spent 9µs making 1 call to Foswiki::UserMapping::new
60
6112µs my $implPasswordManager = $Foswiki::cfg{PasswordManager};
621900ns $implPasswordManager = 'Foswiki::Users::Password'
63 if ( $implPasswordManager eq 'none' );
64120µs eval "require $implPasswordManager";
# spent 72µs executing statements in string eval
651200ns die $@ if $@;
6614µs110µs $this->{passwords} = $implPasswordManager->new($session);
# spent 10µs making 1 call to Foswiki::Users::Password::new
67
6812µs11µs unless ( $this->{passwords}->readOnly() ) {
# spent 1µs making 1 call to Foswiki::Users::Password::readOnly
69 $this->{session}->enterContext('passwords_modifyable');
70 }
71
72 #SMELL: and this is a second user object
73 #TODO: combine with the one in Foswiki::Users
74 #$this->{U2L} = {};
751500ns $this->{L2U} = {};
761300ns $this->{U2W} = {};
771900ns $this->{W2U} = {};
781300ns $this->{eachGroupMember} = {};
791800ns $this->{singleGroupMembers} = ();
80
8114µs return $this;
82}
83
84=begin TML
85
86---++ ObjectMethod finish()
87Break circular references.
88
89=cut
90
91# Note to developers; please undef *all* fields in the object explicitly,
92# whether they are references or not. That way this method is "golden
93# documentation" of the live fields in the object.
94
# spent 1.10ms (1.09+10µs) within Foswiki::Users::TopicUserMapping::finish which was called: # once (1.09ms+10µs) by Foswiki::Users::finish at line 166 of /var/www/foswikidev/core/lib/Foswiki/Users.pm
sub finish {
951700ns my $this = shift;
96
9716µs17µs $this->{passwords}->finish() if $this->{passwords};
# spent 7µs making 1 call to Foswiki::Users::Password::finish
981361µs undef $this->{L2U};
991381µs undef $this->{U2W};
1001318µs undef $this->{W2U};
10119µs undef $this->{passwords};
10211µs undef $this->{eachGroupMember};
1031600ns undef $this->{singleGroupMembers};
104117µs13µs $this->SUPER::finish();
# spent 3µs making 1 call to Foswiki::UserMapping::finish
105}
106
107=begin TML
108
109---++ ObjectMethod supportsRegistration () -> false
110return 1 if the UserMapper supports registration (ie can create new users)
111
112=cut
113
114
# spent 800ns within Foswiki::Users::TopicUserMapping::supportsRegistration which was called: # once (800ns+0s) by Foswiki::Users::supportsRegistration at line 236 of /var/www/foswikidev/core/lib/Foswiki/Users.pm
sub supportsRegistration {
11515µs return 1;
116}
117
118=begin TML
119
120---++ ObjectMethod handlesUser ( $cUID, $login, $wikiname) -> $boolean
121
122Called by the Foswiki::Users object to determine which loaded mapping
123to use for a given user.
124
125The user can be identified by any of $cUID, $login or $wikiname. Any of
126these parameters may be undef, and they should be tested in order; cUID
127first, then login, then wikiname. This mapping is special - for backwards
128compatibility, it assumes responsibility for _all_ non BaseMapping users.
129If you're needing to mix the TopicUserMapping with other mappings,
130define $this->{mapping_id} = 'TopicUserMapping_';
131
132=cut
133
134
# spent 31.9ms (15µs+31.9) within Foswiki::Users::TopicUserMapping::handlesUser which was called 3 times, avg 10.6ms/call: # 3 times (15µs+31.9ms) by Foswiki::Users::_getMapping at line 215 of /var/www/foswikidev/core/lib/Foswiki/Users.pm, avg 10.6ms/call
sub handlesUser {
13533µs my ( $this, $cUID, $login, $wikiname ) = @_;
13636µs if ( defined $cUID && !length( $this->{mapping_id} ) ) {
137
138 # Handle all cUIDs if the mapping ID is not defined
139 return 1;
140 }
141 else {
142
143 # Used when (if) TopicUserMapping is subclassed
1441600ns return 1 if ( defined $cUID && $cUID =~ m/^($this->{mapping_id})/ );
145 }
146
147 # Check the login id to see if we know it
14817µs131.9ms return 1 if ( $login && $this->_userReallyExists($login) );
149
150 # Or the wiki name
151 if ($wikiname) {
152 $this->_loadMapping(); # Sorry Sven, has to be done
153 return 1 if defined $this->{W2U}->{$wikiname};
154 }
155
156 return 0;
157}
158
159=begin TML
160
161---++ ObjectMethod login2cUID ($login, $dontcheck) -> $cUID
162
163Convert a login name to the corresponding canonical user name. The
164canonical name can be any string of 7-bit alphanumeric and underscore
165characters, and must correspond 1:1 to the login name.
166(undef on failure)
167
168(if dontcheck is true, return a cUID for a nonexistant user too.
169This is used for registration)
170
171=cut
172
173
# spent 8.81ms (7.21+1.60) within Foswiki::Users::TopicUserMapping::login2cUID which was called 1066 times, avg 8µs/call: # 1064 times (7.20ms+1.59ms) by Foswiki::Users::TopicUserMapping::_cacheUser at line 1598, avg 8µs/call # once (8µs+8µs) by Foswiki::Users::getCanonicalUserID at line 480 of /var/www/foswikidev/core/lib/Foswiki/Users.pm # once (6µs+6µs) by Foswiki::Users::TopicUserMapping::getLoginName at line 212
sub login2cUID {
1741066448µs my ( $this, $login, $dontcheck ) = @_;
175
1761066111µs211µs unless ($dontcheck) {
# spent 11µs making 2 calls to Foswiki::Users::TopicUserMapping::_userReallyExists, avg 5µs/call
177 return unless ( _userReallyExists( $this, $login ) );
178 }
179
18010662.84ms10661.59ms return $this->{mapping_id} . Foswiki::Users::mapLogin2cUID($login);
# spent 1.59ms making 1066 calls to Foswiki::Users::mapLogin2cUID, avg 1µs/call
181}
182
183=begin TML
184
185---++ ObjectMethod getLoginName ($cUID) -> login
186
187Converts an internal cUID to that user's login
188(undef on failure)
189
190=cut
191
192
# spent 38µs (15+23) within Foswiki::Users::TopicUserMapping::getLoginName which was called: # once (15µs+23µs) by Foswiki::Users::getLoginName at line 685 of /var/www/foswikidev/core/lib/Foswiki/Users.pm
sub getLoginName {
19311µs my ( $this, $cUID ) = @_;
194 ASSERT($cUID) if DEBUG;
195
1961300ns my $login = $cUID;
197
198 #can't call userExists - its recursive
199 #return unless (userExists($this, $user));
200
201 # Remove the mapping id in case this is a subclass
2021500ns $login =~ s/$this->{mapping_id}// if $this->{mapping_id};
203
204259µs223µs
# spent 19µs (15+4) within Foswiki::Users::TopicUserMapping::BEGIN@204 which was called: # once (15µs+4µs) by Foswiki::Users::new at line 204
use bytes;
# spent 19µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@204 # spent 4µs making 1 call to bytes::import
205
206 # Reverse the encoding used to generate cUIDs in login2cUID
207 # use bytes to ignore character encoding
20811µs $login =~ s/_([0-9a-f][0-9a-f])/chr(hex($1))/gei;
20925.00ms217µs
# spent 14µs (11+3) within Foswiki::Users::TopicUserMapping::BEGIN@209 which was called: # once (11µs+3µs) by Foswiki::Users::new at line 209
no bytes;
# spent 14µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@209 # spent 3µs making 1 call to bytes::unimport
210
21111µs15µs return unless _userReallyExists( $this, $login );
21212µs112µs return unless ( $cUID eq $this->login2cUID($login) );
# spent 12µs making 1 call to Foswiki::Users::TopicUserMapping::login2cUID
213
214 # Validated
21515µs16µs return Foswiki::Sandbox::untaintUnchecked($login);
# spent 6µs making 1 call to Foswiki::Sandbox::untaintUnchecked
216}
217
218# test if the login is in the WikiUsers topic, or in the password file
219# depending on the AllowLoginNames setting
220
# spent 31.9ms (66µs+31.9) within Foswiki::Users::TopicUserMapping::_userReallyExists which was called 4 times, avg 7.98ms/call: # 2 times (8µs+2µs) by Foswiki::Users::TopicUserMapping::login2cUID at line 176, avg 5µs/call # once (53µs+31.9ms) by Foswiki::Users::TopicUserMapping::handlesUser at line 148 # once (4µs+1000ns) by Foswiki::Users::TopicUserMapping::getLoginName at line 211
sub _userReallyExists {
22142µs my ( $this, $login ) = @_;
222
22343µs if ( $Foswiki::cfg{Register}{AllowLoginName}
224 || $Foswiki::cfg{PasswordManager} eq 'none' )
225 {
226
227 # need to use the WikiUsers file
228446µs431.9ms $this->_loadMapping();
# spent 31.9ms making 4 calls to Foswiki::Users::TopicUserMapping::_loadMapping, avg 7.96ms/call
229416µs return 1 if ( defined( $this->{L2U}->{$login} ) );
230 }
231
232 if ( $this->{passwords}->canFetchUsers() ) {
233
234 # AllowLoginName mapping failed, maybe the user is however
235 # present in the Wiki managed pwd file
236 # can use the password file if available
237 my $pass = $this->{passwords}->fetchPass($login);
238 return unless ( defined($pass) );
239 return if ( $pass eq '0' ); # login invalid... (SMELL: what
240 # does that really mean)
241 return 1;
242 }
243 else {
244 return 0;
245 }
246
247 return 0;
248}
249
250=begin TML
251
252---++ ObjectMethod addUser ($login, $wikiname, $password, $emails) -> $cUID
253
254throws an Error::Simple
255
256Add a user to the persistent mapping that maps from usernames to wikinames
257and vice-versa. The default implementation uses a special topic called
258"WikiUsers" in the users web. Subclasses will provide other implementations
259(usually stubs if they have other ways of mapping usernames to wikinames).
260Names must be acceptable to $Foswiki::cfg{NameFilter}
261$login must *always* be specified. $wikiname may be undef, in which case
262the user mapper should make one up.
263This function must return a *canonical user id* that it uses to uniquely
264identify the user. This can be the login name, or the wikiname if they
265are all guaranteed unigue, or some other string consisting only of 7-bit
266alphanumerics and underscores.
267if you fail to create a new user (for eg your Mapper has read only access),
268 throw Error::Simple(
269 'Failed to add user: '.$ph->error());
270
271=cut
272
273sub addUser {
274 my ( $this, $login, $wikiname, $password, $emails ) = @_;
275
276 ASSERT($login) if DEBUG;
277
278 # SMELL: really ought to be smarter about this e.g. make a wikiword
279 $wikiname ||= $login;
280
281 if ( $this->{passwords}->fetchPass($login) ) {
282
283 # They exist; their password must match
284 unless ( $this->{passwords}->checkPassword( $login, $password ) ) {
285 throw Error::Simple(
286 $this->{session}->i18n->maketext(
287'User exists in the Password Manager, and the password you provided is different from the users current password. You cannot add a user and change the password at the same time.'
288 )
289 );
290 }
291
292 # User exists, and the password was good.
293 }
294 else {
295
296 # add a new user
297
298 unless ( defined($password) ) {
299 require Foswiki::Users;
300 $password = Foswiki::Users::randomPassword();
301 }
302
303 unless ( $this->{passwords}->setPassword( $login, $password ) == 1 ) {
304
305 throw Error::Simple(
306 $this->{session}->i18n->maketext('Failed to add user: ')
307 . $this->{passwords}->error() );
308 }
309 }
310
311 $this->{CACHED} = 0;
312 my $user = $this->_maintainUsersTopic( 'add', $login, $wikiname );
313
314#can't call setEmails here - user may be in the process of being registered
315#TODO; when registration is moved into the mapping, setEmails will happend after the createUserTOpic
316#$this->setEmails( $user, $emails );
317
318 return $user;
319}
320
321=begin TML
322
323---++ ObjectMethod _maintainUsersTopic ( $action, $login, $wikiname )
324
325throws an Error::Simple
326
327Add or remove a user to/from the persistent mapping that maps from usernames to wikinames
328and vice-versa. The default implementation uses a special topic called
329"WikiUsers" in the users web. =cut
330
331=cut
332
333sub _maintainUsersTopic {
334 my ( $this, $action, $login, $wikiname ) = @_;
335
336 my $usersTopicObject;
337
338 if (
339 $this->{session}->topicExists(
340 $Foswiki::cfg{UsersWebName},
341 $Foswiki::cfg{UsersTopicName}
342 )
343 )
344 {
345
346 # Load existing users topic
347 $usersTopicObject = Foswiki::Meta->load(
348 $this->{session},
349 $Foswiki::cfg{UsersWebName},
350 $Foswiki::cfg{UsersTopicName}
351 );
352 }
353 else {
354
355 return undef if ( $action eq 'del' );
356
357 # Construct a new users topic from the template
358 my $templateTopicObject =
359 Foswiki::Meta->load( $this->{session}, $Foswiki::cfg{SystemWebName},
360 'UsersTemplate' );
361 $usersTopicObject = Foswiki::Meta->new(
362 $this->{session}, $Foswiki::cfg{UsersWebName},
363 $Foswiki::cfg{UsersTopicName}, $templateTopicObject->text()
364 );
365
366 $usersTopicObject->copyFrom($templateTopicObject);
367 }
368
369 my $entry = " * $wikiname - ";
370 $entry .= $login . " - " if $login;
371
372 require Foswiki::Time;
373 my $today =
374 Foswiki::Time::formatTime( time(), $Foswiki::cfg{DefaultDateFormat},
375 'gmtime' );
376
377 my $user;
378
379 # add to the mapping caches unless removing a user
380 unless ( $action eq 'del' ) {
381 $user = _cacheUser( $this, $wikiname, $login );
382 ASSERT($user) if DEBUG;
383 }
384
385 # add name alphabetically to list
386
387 # insidelist is used to see if we are before the first record or after the last
388 # 0 before, 1 inside, 2 after
389 my $insidelist = 0;
390 my $input = $usersTopicObject->text();
391 my $output = '';
392 foreach my $line ( split( /\r?\n/, $input || '' ) ) {
393
394 # TODO: I18N fix here once basic auth problem with 8-bit user names is
395 # solved
396 if ($entry) {
397 my ( $web, $name, $odate ) = ( '', '', '' );
398 if ( $line =~
399m/^\s+\*\s($Foswiki::regex{webNameRegex}\.)?($Foswiki::regex{wikiWordRegex})\s*(?:-\s*\w+\s*)?-\s*(.*)/
400 )
401 {
402 $web = $1 || $Foswiki::cfg{UsersWebName};
403 $name = $2;
404 $odate = $3;
405
406 # Filter-in date format matching {DefaultDateFormat}
407 # The admin may have changed the format at some point of time
408 # so we test with a generic format that matches all 4 formats.
409 $odate = ''
410 unless $odate =~ m/^\d+[- .\/]+[A-Za-z0-9]+[- .\/]+\d+$/;
411 $insidelist = 1;
412 }
413 elsif ( $line =~ m/^\s+\*\s([A-Z]) - / ) {
414
415 # * A - <a name="A">- - - -</a>^M
416 $name = $1;
417 $insidelist = 1;
418 }
419 elsif ( $insidelist == 1 ) {
420
421 # After last entry we have a blank line or some comment
422 # We assume no blank lines inside the list of users
423 # We cannot look for last after Z because Z is not the last letter
424 # in all alphabets
425 $insidelist = 2;
426 $name = '';
427 }
428 if ( ( $name && ( $wikiname le $name ) ) || $insidelist == 2 ) {
429
430 # found alphabetical position or last record
431 if ( $wikiname eq $name ) {
432
433 next if ( $action eq 'del' );
434
435 # adjusting existing user - keep original registration date
436 $entry .= $odate;
437 }
438 else {
439 $entry .= $today . "\n" . $line;
440 }
441
442 # don't adjust if unchanged
443 return $user if ( $entry eq $line && $action eq 'add' );
444 $line = $entry if ( $action eq 'add' );
445 $entry = '';
446 }
447 }
448 $output .= $line . "\n";
449 }
450
451 if ( $entry && $action eq 'add' ) {
452
453 # brand new file - add to end
454 $output .= "$entry$today\n";
455 }
456 $usersTopicObject->text($output);
457
458 $this->{CACHED} = 0;
459 try {
460 $usersTopicObject->save(
461 author =>
462
463 # SMELL: why is this Admin and not the RegoAgent??
464 $this->{session}->{users}
465 ->getCanonicalUserID( $Foswiki::cfg{AdminUserLogin} )
466 );
467 }
468 catch Error with {
469
470 # Failed to add user; must remove them from the password system too,
471 # otherwise their next registration attempt will be blocked
472 my $e = shift;
473 $this->{passwords}->removeUser($login);
474 throw $e;
475 };
476
477 return $user;
478}
479
480=begin TML
481
482---++ ObjectMethod removeUser( $cUID ) -> $boolean
483
484Delete the users entry. Removes the user from the password
485manager and user mapping manager. Does *not* remove their personal
486topics, which may still be linked.
487
488Note that this must be called with the cUID. If any doubt, resolve the cUID
489by $this->{session}->{users}->getCanonicalUserID($identity).
490
491=cut
492
493sub removeUser {
494 my ( $this, $cUID ) = @_;
495
496 my $ln = $this->getLoginName($cUID);
497 my $wikiname = $this->getWikiName($cUID);
498 $this->{passwords}->removeUser($ln) if ($ln);
499
500 # SMELL: If for some reason the login or wikiname is not found in the mapping
501 # Then the WikiUsers topic will not be maintained.
502 $this->_maintainUsersTopic( 'del', $ln, $wikiname ) if ( $ln && $wikiname );
503
504 # SMELL: does not update the internal caches,
505 # needs someone to implement it
506 # Brutal update - invalidate them all
507
508 $this->{CACHED} = 0;
509 $this->{L2U} = {};
510 $this->{U2W} = {};
511 $this->{W2U} = {};
512 $this->{eachGroupMember} = {};
513 $this->{singleGroupMembers} = ();
514
515 return 1;
516
517}
518
519=begin TML
520
521---++ ObjectMethod getWikiName ($cUID) -> $wikiname
522
523Map a canonical user name to a wikiname. If it fails to find a
524WikiName, it will attempt to find a matching loginname, and use
525an escaped version of that.
526If there is no matching WikiName or LoginName, it returns undef.
527
528=cut
529
530
# spent 10µs (9+900ns) within Foswiki::Users::TopicUserMapping::getWikiName which was called: # once (9µs+900ns) by Foswiki::Users::getWikiName at line 716 of /var/www/foswikidev/core/lib/Foswiki/Users.pm
sub getWikiName {
53111µs my ( $this, $cUID ) = @_;
532 ASSERT($cUID) if DEBUG;
5331100ns ASSERT( $cUID =~ m/^$this->{mapping_id}/ ) if DEBUG;
534
5351300ns my $wikiname;
536
53711µs if ( $Foswiki::cfg{Register}{AllowLoginName} ) {
5381800ns1900ns $this->_loadMapping();
# spent 900ns making 1 call to Foswiki::Users::TopicUserMapping::_loadMapping
5391900ns $wikiname = $this->{U2W}->{$cUID};
540 }
541 else {
542
543 # If the mapping isn't enabled there's no point in loading it
544 }
545
5461300ns unless ($wikiname) {
547 $wikiname = $this->getLoginName($cUID);
548 if ($wikiname) {
549
550 # sanitise the generated WikiName
551 $wikiname =~ s/$Foswiki::cfg{NameFilter}//g;
552 }
553 }
554
55514µs return $wikiname;
556}
557
558=begin TML
559
560---++ ObjectMethod userExists($cUID) -> $boolean
561
562Determine if the user already exists or not. Whether a user exists
563or not is determined by the password manager.
564
565=cut
566
567sub userExists {
568 my ( $this, $cUID ) = @_;
569 ASSERT($cUID) if DEBUG;
570
571 # Do this to avoid a password manager lookup
572 return 1 if $cUID eq $this->{session}->{user};
573
574 my $loginName = $this->getLoginName($cUID);
575 return 0 unless defined($loginName);
576
577 return 1 if ( $loginName eq $Foswiki::cfg{DefaultUserLogin} );
578
579 # Foswiki allows *groups* to log in
580 return 1 if ( $this->isGroup($loginName) );
581
582 # Look them up in the password manager (can be slow).
583 return 1
584 if ( $this->{passwords}->canFetchUsers()
585 && $this->{passwords}->fetchPass($loginName) );
586
587 unless ( $Foswiki::cfg{Register}{AllowLoginName}
588 && $this->{passwords}->canFetchUsers() )
589 {
590
591 #if there is no pwd file, then its external auth
592 #and if AllowLoginName is also off, then the only way to know if
593 #the user has registered is to test for user topic?
594 my $wikiname = $this->{session}->{users}->getWikiName($cUID);
595 if (
596 Foswiki::Func::topicExists(
597 $Foswiki::cfg{UsersWebName}, $wikiname
598 )
599 )
600 {
601 return 1;
602 }
603 }
604
605 return 0;
606}
607
608=begin TML
609
610---++ ObjectMethod eachUser () -> Foswiki::Iterator of cUIDs
611
612See baseclass for documentation
613
614=cut
615
616sub eachUser {
617 my ($this) = @_;
618
619 $this->_loadMapping();
620 my @list = keys( %{ $this->{U2W} } );
621 my $iter = new Foswiki::ListIterator( \@list );
622 $iter->{filter} = sub {
623
624 # don't claim users that are handled by the basemapping
625 my $cUID = $_[0] || '';
626 my $login = $this->{session}->{users}->getLoginName($cUID);
627 my $wikiname = $this->{session}->{users}->getWikiName($cUID);
628
629 return !( $this->{session}->{users}->{basemapping}
630 ->handlesUser( undef, $login, $wikiname ) );
631 };
632 return $iter;
633}
634
635=begin TML
636
637---++ ObjectMethod eachGroupMember ($group) -> listIterator of cUIDs
638
639See baseclass for documentation
640
641=cut
642
6431200nsmy %expanding; # Prevents loops in nested groups
644
645sub eachGroupMember {
646 my ( $this, $group, $options ) = @_;
647
648 my $expand = $options->{expand};
649
650 if ( Scalar::Util::tainted($group) ) {
651 $group = Foswiki::Sandbox::untaint( $group,
652 \&Foswiki::Sandbox::validateTopicName );
653 }
654
655 $expand = 1 unless ( defined $expand );
656
657 # print STDERR "eachGroupMember called for $group - expand $expand \n";
658
659 if ( !$expand && defined( $this->{singleGroupMembers}->{$group} ) ) {
660
661 # print STDERR "Returning cached unexpanded list for $group\n";
662 return new Foswiki::ListIterator(
663 $this->{singleGroupMembers}->{$group} );
664 }
665
666 if ( $expand && defined( $this->{eachGroupMember}->{$group} ) ) {
667
668 # print STDERR "Returning cached expanded list for $group\n";
669 return new Foswiki::ListIterator( $this->{eachGroupMember}->{$group} );
670 }
671
672 # print "Cache miss for $group expand $expand \n";
673
674 my $session = $this->{session};
675 my $users = $session->{users};
676
677 my $members = [];
678 my $singleGroupMembers = [];
679
680# Determine if we are called recursively, either directly, or by the _expandUserList routine
681 unless ( ( caller(1) )[3] eq ( caller(0) )[3]
682 || ( caller(2) )[3] eq ( caller(0) )[3] )
683 {
684
685 # print "eachGroupMember $group - TOP LEVEL \n";
686 %expanding = ();
687 }
688
689 if ( !$expanding{$group}
690 && $session->topicExists( $Foswiki::cfg{UsersWebName}, $group ) )
691 {
692 $expanding{$group} = 1;
693
694 # print "Expanding $group \n";
695 my $groupTopicObject =
696 Foswiki::Meta->load( $this->{session}, $Foswiki::cfg{UsersWebName},
697 $group );
698
699 if ( !$expand ) {
700 $singleGroupMembers =
701 _expandUserList( $this,
702 $groupTopicObject->getPreference('GROUP'), 0 );
703 $this->{singleGroupMembers}->{$group} = $singleGroupMembers;
704
705# print "Returning iterator for singleGroupMembers $group, members $singleGroupMembers \n";
706 return new Foswiki::ListIterator(
707 $this->{singleGroupMembers}->{$group} );
708 }
709 else {
710 $members =
711 _expandUserList( $this,
712 $groupTopicObject->getPreference('GROUP') );
713 $this->{eachGroupMember}->{$group} = $members;
714 }
715
716 delete $expanding{$group};
717 }
718
719 # print "Returning iterator for eachGroupMember $group \n";
720 return new Foswiki::ListIterator( $this->{eachGroupMember}->{$group} );
721}
722
723=begin TML
724
725---++ ObjectMethod isGroup ($user) -> boolean
726
727See baseclass for documentation
728
729=cut
730
731
# spent 165µs within Foswiki::Users::TopicUserMapping::isGroup which was called 62 times, avg 3µs/call: # 62 times (165µs+0s) by Foswiki::Users::isGroup at line 845 of /var/www/foswikidev/core/lib/Foswiki/Users.pm, avg 3µs/call
sub isGroup {
7326225µs my ( $this, $user ) = @_;
733
734 # Groups have the same username as wikiname as canonical name
7356231µs return 1 if $user eq $Foswiki::cfg{SuperAdminGroup};
736
73762163µs return 0 unless ( $user =~ m/Group$/ );
738
739 #actually test for the existance of this group
740 #TODO: SMELL: this is still a lie, because it will claim that a
741 #Group which the currently logged in user does _not_
742 #have VIEW permission for simply is non-existant.
743 #however, this may be desirable for security reasons.
744 #SMELL: this is why we should not use topicExist to test for createability...
745 my $iterator = $this->eachGroup();
746 while ( $iterator->hasNext() ) {
747 my $groupname = $iterator->next();
748 return 1 if ( $groupname eq $user );
749 }
750 return 0;
751}
752
753=begin TML
754
755---++ ObjectMethod eachGroup () -> ListIterator of groupnames
756
757See baseclass for documentation
758
759=cut
760
761sub eachGroup {
762 my ($this) = @_;
763 _getListOfGroups($this);
764 return new Foswiki::ListIterator( \@{ $this->{groupsList} } );
765}
766
767=begin TML
768
769---++ ObjectMethod eachMembership ($cUID) -> ListIterator of groups this user is in
770
771See baseclass for documentation
772
773=cut
774
775sub eachMembership {
776 my ( $this, $user ) = @_;
777
778 _getListOfGroups($this);
779 my $it = new Foswiki::ListIterator( \@{ $this->{groupsList} } );
780 $it->{filter} = sub {
781 $this->isInGroup( $user, $_[0] );
782 };
783 return $it;
784}
785
786=begin TML
787
788---++ ObjectMethod groupAllowsView($group) -> boolean
789
790returns 1 if the group is able to be viewed by the current logged in user
791
792implemented using topic VIEW permissions
793
794=cut
795
796sub groupAllowsView {
797 my $this = shift;
798 my $Group = shift;
799
800 my $user = $this->{session}->{user};
801 return 1 if $this->{session}->{users}->isAdmin($user);
802
803 $Group = Foswiki::Sandbox::untaint( $Group,
804 \&Foswiki::Sandbox::validateTopicName );
805 my ( $groupWeb, $groupName ) =
806 $this->{session}
807 ->normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, $Group );
808
809# If a Group or User topic normalized somewhere else, doesn't make sense, so ignore the Webname
810 $groupWeb = $Foswiki::cfg{UsersWebName};
811
812 $groupName = undef
813 if ( not $this->{session}->topicExists( $groupWeb, $groupName ) );
814
815 return Foswiki::Func::checkAccessPermission( 'VIEW', $user, undef,
816 $groupName, $groupWeb );
817}
818
819=begin TML
820
821---++ ObjectMethod groupAllowsChange($group, $cuid) -> boolean
822
823returns 1 if the group is able to be modified by $cuid
824
825implemented using topic CHANGE permissions
826
827=cut
828
829sub groupAllowsChange {
830 my $this = shift;
831 my $Group = shift;
832 my $user = shift;
833 ASSERT( defined $user ) if DEBUG;
834
835 $Group = Foswiki::Sandbox::untaint( $Group,
836 \&Foswiki::Sandbox::validateTopicName );
837 my ( $groupWeb, $groupName ) =
838 $this->{session}
839 ->normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, $Group );
840
841 # SMELL: Should NobodyGroup be configurable?
842 return 0 if $groupName eq 'NobodyGroup';
843 return 1 if $this->{session}->{users}->isAdmin($user);
844
845# If a Group or User topic normalized somewhere else, doesn't make sense, so ignore the Webname
846 $groupWeb = $Foswiki::cfg{UsersWebName};
847
848 $groupName = undef
849 if ( not $this->{session}->topicExists( $groupWeb, $groupName ) );
850
851 return Foswiki::Func::checkAccessPermission( 'CHANGE', $user, undef,
852 $groupName, $groupWeb );
853}
854
855=begin TML
856
857---++ ObjectMethod addToGroup( $cuid, $group, $create ) -> $boolean
858adds the user specified by the cuid to the group.
859If the group does not exist, it will return false and do nothing, unless the create flag is set.
860
861cuid be a groupname which is added like it was an unknown user
862
863=cut
864
865sub addUserToGroup {
866 my ( $this, $cuid, $Group, $create ) = @_;
867 $Group = Foswiki::Sandbox::untaint( $Group,
868 \&Foswiki::Sandbox::validateTopicName );
869 my ( $groupWeb, $groupName ) =
870 $this->{session}
871 ->normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, $Group );
872
873 throw Error::Simple( $this->{session}
874 ->i18n->maketext( 'Users cannot be added to [_1]', $Group ) )
875 if ( $Group eq 'NobodyGroup' || $Group eq 'BaseGroup' );
876
877 throw Error::Simple(
878 $this->{session}->i18n->maketext('Group names must end in Group') )
879 unless ( $Group =~ m/Group$/ );
880
881 # the registration code will call this function using the rego agent
882 my $user = $this->{session}->{user};
883
884 my $usersObj = $this->{session}->{users};
885
886 print STDERR "$user, aka("
887 . $usersObj->getWikiName($user)
888 . ") is TRYING to add $cuid aka("
889 . $usersObj->getWikiName($cuid)
890 . ") to $groupName\n"
891 if ( $cuid && DEBUG );
892
893 my $membersString = '';
894 my $allowChangeString;
895 my $groupTopicObject;
896
897 if ( $usersObj->isGroup($groupName) ) {
898
899 $groupTopicObject =
900 Foswiki::Meta->load( $this->{session}, $groupWeb, $groupName );
901
902 if ( !$groupTopicObject->haveAccess( 'CHANGE', $user ) ) {
903 throw Error::Simple( $this->{session}
904 ->i18n->maketext( 'CHANGE not permitted by [_1]', $user ) );
905 }
906
907 $membersString = $groupTopicObject->getPreference('GROUP') || '';
908
909 my @l;
910 foreach my $ident ( split( /[\,\s]+/, $membersString ) ) {
911 $ident =~ s/^($Foswiki::cfg{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//;
912 push( @l, $ident ) if $ident;
913 }
914 $membersString = join( ', ', @l );
915
916 if ( $create and !defined($cuid) ) {
917
918 #upgrade group topic.
919 $this->_writeGroupTopic(
920 $groupTopicObject, $groupWeb, $groupName,
921 $membersString, $allowChangeString
922 );
923
924 return 1;
925 }
926 }
927 else {
928
929# see if we have permission to add a topic, or to edit the existing topic, etc..
930
931 throw Error::Simple( $this->{session}
932 ->i18n->maketext('Group does not exist and create not permitted')
933 ) unless ($create);
934
935 throw Error::Simple(
936 $this->{session}->i18n->maketext(
937 'CHANGE not permitted for [_1] by [_2]',
938 ( $groupName, $user )
939 )
940 )
941 unless (
942 Foswiki::Func::checkAccessPermission(
943 'CHANGE', $user, '', $groupName, $groupWeb
944 )
945 );
946
947 $groupTopicObject =
948 Foswiki::Meta->load( $this->{session}, $groupWeb, 'GroupTemplate' );
949
950 # expand the GroupTemplate as best we can.
951 $this->{session}->{request}
952 ->param( -name => 'topic', -value => $groupName );
953 $groupTopicObject->expandNewTopic();
954
955 $allowChangeString = $groupName;
956 }
957
958 my $wikiName = '';
959 $wikiName = $usersObj->getWikiName($cuid) if ($cuid);
960
961 if ( $membersString !~ m/\b$wikiName\b/ ) {
962 $membersString .= ', ' if ( $membersString ne '' );
963 $membersString .= $wikiName;
964 }
965
966 Foswiki::Func::writeEvent( 'addUserToGroup',
967 "$groupName: $wikiName added by $user" );
968
969 $this->_clearGroupCache($groupName);
970
971 $this->_writeGroupTopic(
972 $groupTopicObject, $groupWeb, $groupName,
973 $membersString, $allowChangeString
974 );
975
976 # reparse groups brute force :/
977 _getListOfGroups( $this, 1 ) if ($create);
978 return 1;
979}
980
981#start by just writing the new form.
982sub _writeGroupTopic {
983 my $this = shift;
984 my $groupTopicObject = shift;
985 my $groupWeb = shift;
986 my $groupName = shift;
987 my $membersString = shift;
988 my $allowChangeString = shift;
989
990 my $text = $groupTopicObject->text() || '';
991
992#TODO: do an attempt to convert existing old style topics - compare to 'normal' GroupTemplate? (I'm hoping to keep any user added descriptions for the group
993 if (
994 (
995 !defined $groupTopicObject->getPreference('VIEW_TEMPLATE')
996 or $groupTopicObject->getPreference('VIEW_TEMPLATE') ne 'GroupView'
997 )
998 or ( $text =~ m/^---\+!! <nop>.*$/ )
999 or ( $text =~ m/^(\t| )+\* Set GROUP = .*$/ )
1000 or ( $text =~ m/^(\t| )+\* Member list \(comma-separated list\):$/ )
1001 or ( $text =~ m/^(\t| )+\* Persons\/group who can change the list:$/ )
1002 or ( $text =~ m/^(\t| )+\* Set ALLOWTOPICCHANGE = .*$/ )
1003 or ( $text =~ m/^\*%MAKETEXT\{"Related topics:"\}%.*$/ )
1004 )
1005 {
1006 if ( !defined($allowChangeString) ) {
1007 $allowChangeString =
1008 $groupTopicObject->getPreference('ALLOWTOPICCHANGE') || '';
1009 }
1010
1011 $text =~ s/^---\+!! <nop>.*$//s;
1012 $text =~ s/^(\t| )+\* Set GROUP = .*$//s;
1013 $text =~ s/^(\t| )+\* Member list \(comma-separated list\):$//s;
1014 $text =~ s/^(\t| )+\* Persons\/group who can change the list:$//s;
1015 $text =~ s/^(\t| )+\* Set ALLOWTOPICCHANGE = .*$//s;
1016 $text =~ s/^\*%MAKETEXT\{"Related topics:"\}%.*$//s;
1017
1018 $text .= "\nEdit this topic to add a description to the $groupName\n";
1019
1020#TODO: consider removing the VIEW_TEMPLATE that only very few people should ever have...
1021 }
1022
1023 $groupTopicObject->text($text);
1024
1025 $groupTopicObject->putKeyed(
1026 'PREFERENCE',
1027 {
1028 type => 'Set',
1029 name => 'GROUP',
1030 title => 'GROUP',
1031 value => $membersString
1032 }
1033 );
1034 if ( defined($allowChangeString) ) {
1035 $groupTopicObject->putKeyed(
1036 'PREFERENCE',
1037 {
1038 type => 'Set',
1039 name => 'ALLOWTOPICCHANGE',
1040 title => 'ALLOWTOPICCHANGE',
1041 value => $allowChangeString
1042 }
1043 );
1044 }
1045 $groupTopicObject->putKeyed(
1046 'PREFERENCE',
1047 {
1048 type => 'Set',
1049 name => 'VIEW_TEMPLATE',
1050 title => 'VIEW_TEMPLATE',
1051 value => 'GroupView'
1052 }
1053 );
1054
1055 #TODO: should also consider securing the new topic?
1056 my $user = $this->{session}->{user};
1057 $groupTopicObject->saveAs(
1058 web => $groupWeb,
1059 topic => $groupName,
1060 author => $user,
1061 forcenewrevision => ( $groupName eq $Foswiki::cfg{SuperAdminGroup} )
1062 ? 1
1063 : 0
1064 );
1065
1066}
1067
1068=begin TML
1069
1070---++ ObjectMethod removeFromGroup( $cuid, $group ) -> $boolean
1071
1072=cut
1073
1074sub removeUserFromGroup {
1075 my ( $this, $cuid, $groupName ) = @_;
1076 $groupName = Foswiki::Sandbox::untaint( $groupName,
1077 \&Foswiki::Sandbox::validateTopicName );
1078 my ( $groupWeb, $groupTopic ) =
1079 $this->{session}
1080 ->normalizeWebTopicName( $Foswiki::cfg{UsersWebName}, $groupName );
1081
1082 throw Error::Simple( $this->{session}
1083 ->i18n->maketext( 'Users cannot be removed from [_1]', $groupName ) )
1084 if ( $groupName eq 'BaseGroup' );
1085
1086 throw Error::Simple(
1087 $this->{session}->i18n->maketext(
1088 '[_1] cannot be removed from [_2]',
1089 (
1090 $Foswiki::cfg{AdminUserWikiName}, $Foswiki::cfg{SuperAdminGroup}
1091 )
1092 )
1093 )
1094 if ( $groupName eq "$Foswiki::cfg{SuperAdminGroup}"
1095 && $cuid eq 'BaseUserMapping_333' );
1096
1097 my $user = $this->{session}->{user};
1098 my $usersObj = $this->{session}->{users};
1099
1100 if (
1101 $usersObj->isGroup($groupName)
1102 and ( $this->{session}
1103 ->topicExists( $Foswiki::cfg{UsersWebName}, $groupName ) )
1104 )
1105 {
1106 if ( !$usersObj->isInGroup( $cuid, $groupName, { expand => 0 } )
1107 && !$usersObj->isGroup($cuid) )
1108 {
1109
1110 throw Error::Simple(
1111 $this->{session}->i18n->maketext(
1112 'User [_1] not in group, cannot be removed', $cuid
1113 )
1114 );
1115 }
1116 my $groupTopicObject =
1117 Foswiki::Meta->load( $this->{session}, $Foswiki::cfg{UsersWebName},
1118 $groupName );
1119 if ( !$groupTopicObject->haveAccess( 'CHANGE', $user ) ) {
1120
1121 throw Error::Simple(
1122 $this->{session}->i18n->maketext(
1123 'User [_1] does not have CHANGE permission on [_2].',
1124 ( $user, $groupName )
1125 )
1126 );
1127 }
1128
1129 my $WikiName = $usersObj->getWikiName($cuid);
1130 my $LoginName = $usersObj->getLoginName($cuid) || '';
1131
1132 my $membersString = $groupTopicObject->getPreference('GROUP');
1133 my @l;
1134 foreach my $ident ( split( /[\,\s]+/, $membersString ) ) {
1135 $ident =~ s/^($Foswiki::cfg{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//;
1136 next if ( $ident eq $WikiName );
1137 next if ( $ident eq $LoginName );
1138 next if ( $ident eq $cuid );
1139 push( @l, $ident );
1140 }
1141 $membersString = join( ', ', @l );
1142
1143 Foswiki::Func::writeEvent( 'removeUserFromGroup',
1144 "$groupTopic: $WikiName removed by $user" );
1145
1146 $this->_writeGroupTopic( $groupTopicObject, $groupWeb, $groupTopic,
1147 $membersString );
1148
1149 $this->_clearGroupCache($groupName);
1150
1151 return 1;
1152 }
1153
1154 return 0;
1155}
1156
1157=begin TML
1158
1159---++ ObjectMethod _clearGroupCache( $groupName )
1160
1161Removes the cache entries for unexpanded and expanded groups,
1162and searches un-expanded groups for any nesting group references
1163clearing them as well.
1164
1165Note: This is not recursive and does not attempt to handle
1166more than one level of nested groups.
1167
1168=cut
1169
1170sub _clearGroupCache {
1171 my ( $this, $groupName ) = @_;
1172
1173 delete $this->{eachGroupMember}->{$groupName};
1174 delete $this->{singleGroupMembers}->{$groupName};
1175
1176 #SMELL: This should probably be recursive.
1177 foreach my $groupKey ( keys( %{ $this->{singleGroupMembers} } ) ) {
1178 if ( $this->{singleGroupMembers}->{$groupKey} =~ m/$groupName/ ) {
1179
1180 # print STDERR "Deleting cache for $groupKey \n";
1181 delete $this->{eachGroupMember}->{$groupKey};
1182 delete $this->{singleGroupMembers}->{$groupKey};
1183 }
1184 }
1185}
1186
1187=begin TML
1188
1189---++ ObjectMethod isAdmin( $cUID ) -> $boolean
1190
1191True if the user is an admin
1192 * is $Foswiki::cfg{SuperAdminGroup}
1193 * is a member of the $Foswiki::cfg{SuperAdminGroup}
1194
1195=cut
1196
1197sub isAdmin {
1198 my ( $this, $cUID ) = @_;
1199 my $isAdmin = 0;
1200
1201 # TODO: this might not apply now that we have BaseUserMapping - test
1202 if ( $cUID eq $Foswiki::cfg{SuperAdminGroup} ) {
1203 $isAdmin = 1;
1204 }
1205 else {
1206 my $sag = $Foswiki::cfg{SuperAdminGroup};
1207 $isAdmin = $this->isInGroup( $cUID, $sag );
1208 }
1209
1210 return $isAdmin;
1211}
1212
1213=begin TML
1214
1215---++ ObjectMethod findUserByEmail( $email ) -> \@cUIDs
1216 * =$email= - email address to look up
1217Return a list of canonical user names for the users that have this email
1218registered with the password manager or the user mapping manager.
1219
1220The password manager is asked first for whether it maps emails.
1221If it doesn't, then the user mapping manager is asked instead.
1222
1223=cut
1224
1225sub findUserByEmail {
1226 my ( $this, $email ) = @_;
1227 ASSERT($email) if DEBUG;
1228 my @users;
1229 if ( !$Foswiki::cfg{TopicUserMapping}{ForceManageEmails}
1230 && $this->{passwords}->isManagingEmails() )
1231 {
1232 my $logins = $this->{passwords}->findUserByEmail($email);
1233 if ( defined $logins ) {
1234 foreach my $l (@$logins) {
1235 $l = $this->login2cUID($l);
1236 push( @users, $l ) if $l;
1237 }
1238 }
1239 }
1240 else {
1241
1242 # if the password manager didn't want to provide the service, ask
1243 # the user mapping manager
1244 unless ( $this->{_MAP_OF_EMAILS} ) {
1245 $this->{_MAP_OF_EMAILS} = {};
1246 my $it = $this->eachUser();
1247 while ( $it->hasNext() ) {
1248 my $uo = $it->next();
1249 map { push( @{ $this->{_MAP_OF_EMAILS}->{$_} }, $uo ); }
1250 $this->getEmails($uo);
1251 }
1252 }
1253 push( @users, @{ $this->{_MAP_OF_EMAILS}->{$email} } )
1254 if ( $this->{_MAP_OF_EMAILS}->{$email} );
1255 }
1256 return \@users;
1257}
1258
1259=begin TML
1260
1261---++ ObjectMethod getEmails($name) -> @emailAddress
1262
1263If $name is a user, return their email addresses. If it is a group,
1264return the addresses of everyone in the group.
1265
1266The password manager and user mapping manager are both consulted for emails
1267for each user (where they are actually found is implementation defined).
1268
1269Duplicates are removed from the list.
1270
1271=cut
1272
1273sub getEmails {
1274 my ( $this, $user, $seen ) = @_;
1275
1276 $seen ||= {};
1277
1278 my %emails = ();
1279
1280 if ( $seen->{$user} ) {
1281
1282 #print STDERR "preventing infinit recursion in getEmails($user)\n";
1283 }
1284 else {
1285 $seen->{$user} = 1;
1286
1287 if ( $this->isGroup($user) ) {
1288 my $it = $this->eachGroupMember($user);
1289 while ( $it->hasNext() ) {
1290 foreach ( $this->getEmails( $it->next(), $seen ) ) {
1291 $emails{$_} = 1;
1292 }
1293 }
1294 }
1295 else {
1296 if ( !$Foswiki::cfg{TopicUserMapping}{ForceManageEmails}
1297 && $this->{passwords}->isManagingEmails() )
1298 {
1299
1300 # get emails from the password manager
1301 foreach ( $this->{passwords}
1302 ->getEmails( $this->getLoginName($user), $seen ) )
1303 {
1304 $emails{$_} = 1;
1305 }
1306 }
1307 else {
1308
1309 # And any on offer from the user mapping manager
1310 foreach ( mapper_getEmails( $this->{session}, $user ) ) {
1311 $emails{$_} = 1;
1312 }
1313 }
1314 }
1315 }
1316 return keys %emails;
1317}
1318
1319=begin TML
1320
1321---++ ObjectMethod setEmails($cUID, @emails) -> boolean
1322
1323Set the email address(es) for the given user.
1324The password manager is tried first, and if it doesn't want to know the
1325user mapping manager is tried.
1326
1327=cut
1328
1329sub setEmails {
1330 my $this = shift;
1331 my $user = shift;
1332
1333 if ( !$Foswiki::cfg{TopicUserMapping}{ForceManageEmails}
1334 && $this->{passwords}->isManagingEmails() )
1335 {
1336 $this->{passwords}->setEmails( $this->getLoginName($user), @_ );
1337 }
1338 else {
1339 mapper_setEmails( $this->{session}, $user, @_ );
1340 }
1341}
1342
1343=begin TML
1344
1345---++ StaticMethod mapper_getEmails($session, $user)
1346
1347Only used if passwordManager->isManagingEmails= = =false or
1348$Foswiki::cfg{TopicUserMapping}{ForceManageEmails} is enabled.
1349(The emails are stored in the user topics.
1350
1351Note: This method is PUBLIC because it is used by the tools/upgrade_emails.pl
1352script, which needs to kick down to the mapper to retrieve email addresses
1353from Wiki topics.
1354
1355=cut
1356
1357sub mapper_getEmails {
1358 my ( $session, $user ) = @_;
1359
1360 my $topicObject = Foswiki::Meta->load(
1361 $session,
1362 $Foswiki::cfg{UsersWebName},
1363 $session->{users}->getWikiName($user)
1364 );
1365
1366 my @addresses;
1367
1368 # Try the form first
1369 my $entry = $topicObject->get( 'FIELD', 'Email' );
1370 if ($entry) {
1371 push( @addresses, split( /;/, $entry->{value} ) );
1372 }
1373 elsif ( defined $topicObject->text ) {
1374
1375 # Now try the topic text
1376 foreach my $l ( split( /\r?\n/, $topicObject->text ) ) {
1377 if ( $l =~ m/^\s+\*\s+E-?mail:\s*(.*)$/mi ) {
1378
1379 # SMELL: implicit unvalidated untaint
1380 push @addresses, split( /;/, $1 );
1381 }
1382 }
1383 }
1384
1385 return @addresses;
1386}
1387
1388=begin TML
1389
1390---++ StaticMethod mapper_setEmails ($session, $user, @emails)
1391
1392Only used if =passwordManager->isManagingEmails= = =false=
1393or $Foswiki::cfg{TopicUserMapping}{ForceManageEmails} is enabled.
1394(emails are stored in user topics
1395
1396=cut
1397
1398sub mapper_setEmails {
1399 my $session = shift;
1400 my $cUID = shift;
1401
1402 my $mails = join( ';', @_ );
1403
1404 my $user = $session->{users}->getWikiName($cUID);
1405
1406 my $topicObject =
1407 Foswiki::Meta->load( $session, $Foswiki::cfg{UsersWebName}, $user );
1408
1409 if ( $topicObject->get('FORM') ) {
1410
1411 # use the form if there is one
1412 $topicObject->putKeyed(
1413 'FIELD',
1414 {
1415 name => 'Email',
1416 value => $mails,
1417 title => 'Email',
1418 attributes => 'h'
1419 }
1420 );
1421 }
1422 else {
1423
1424 # otherwise use the topic text
1425 my $text = $topicObject->text() || '';
1426 unless ( $text =~ s/^(\s+\*\s+E-?mail:\s*).*$/$1$mails/mi ) {
1427 $text .= "\n * Email: $mails\n";
1428 }
1429 $topicObject->text($text);
1430 }
1431
1432 $topicObject->save();
1433}
1434
1435=begin TML
1436
1437---++ ObjectMethod findUserByWikiName ($wikiname) -> list of cUIDs associated with that wikiname
1438
1439See baseclass for documentation
1440
1441The $skipExistanceCheck parameter
1442is private to this module, and blocks the standard existence check
1443to avoid reading .htpasswd when checking group memberships).
1444
1445=cut
1446
1447sub findUserByWikiName {
1448 my ( $this, $wn, $skipExistanceCheck ) = @_;
1449 my @users = ();
1450
1451 if ( $this->isGroup($wn) ) {
1452 push( @users, $wn );
1453 }
1454 elsif ( $Foswiki::cfg{Register}{AllowLoginName} ) {
1455
1456 # print STDERR "AllowLoginName discovered \n";
1457
1458 # Add additional mappings defined in WikiUsers
1459 $this->_loadMapping();
1460 if ( $this->{W2U}->{$wn} ) {
1461
1462 # Wikiname to UID mapping is defined
1463 my $user = $this->{W2U}->{$wn};
1464 push( @users, $user ) if $user;
1465 }
1466 else {
1467
1468 # Bloody compatibility!
1469 # The wikiname is always a registered user for the purposes of this
1470 # mapping. We have to do this because Foswiki defines access controls
1471 # in terms of mapped users, and if a wikiname is *missing* from the
1472 # mapping there is "no such user".
1473 my $user = $this->login2cUID($wn);
1474 push( @users, $user ) if $user;
1475 }
1476 }
1477 else {
1478
1479 # print STDERR "NOT AllowLoginName \n";
1480
1481 # The wikiname is also the login name, so we can just convert
1482 # it directly to a cUID
1483 my $cUID = $this->login2cUID($wn);
1484
1485 # print STDERR "login2cUID for $wn returned $cUID \n";
1486
1487 # print STDERR "$wn EXISTS \n" if ( $cUID && $this->userExists($cUID) );
1488 if ( $skipExistanceCheck || ( $cUID && $this->userExists($cUID) ) ) {
1489 push( @users, $cUID );
1490 }
1491 }
1492 return \@users;
1493}
1494
1495=begin TML
1496
1497---++ ObjectMethod checkPassword( $login, $password ) -> $boolean
1498
1499Finds if the password is valid for the given user.
1500
1501Returns 1 on success, undef on failure.
1502
1503=cut
1504
1505sub checkPassword {
1506 my ( $this, $login, $pw ) = @_;
1507
1508 # If we don't have a PasswordManager and use TemplateLogin, always allow login
1509 return 1
1510 if ( $Foswiki::cfg{PasswordManager} eq 'none'
1511 && $Foswiki::cfg{LoginManager} eq
1512 'Foswiki::LoginManager::TemplateLogin' );
1513
1514 return $this->{passwords}->checkPassword( $login, $pw );
1515}
1516
1517=begin TML
1518
1519---++ ObjectMethod setPassword( $cUID, $newPassU, $oldPassU ) -> $boolean
1520
1521BEWARE: $user should be a cUID, but is a login when the resetPassword
1522functionality is used.
1523The UserMapper needs to convert either one to a valid login for use by
1524the Password manager
1525
1526TODO: needs fixing
1527
1528If the $oldPassU matches matches the user's password, then it will
1529replace it with $newPassU.
1530
1531If $oldPassU is not correct and not 1, will return 0.
1532
1533If $oldPassU is 1, will force the change irrespective of
1534the existing password, adding the user if necessary.
1535
1536Otherwise returns 1 on success, undef on failure.
1537
1538=cut
1539
1540sub setPassword {
1541 my ( $this, $user, $newPassU, $oldPassU ) = @_;
1542 ASSERT($user) if DEBUG;
1543 my $login = $this->getLoginName($user) || $user;
1544 return $this->{passwords}->setPassword( $login, $newPassU, $oldPassU );
1545}
1546
1547=begin TML
1548
1549---++ ObjectMethod passwordError( ) -> $string
1550
1551returns a string indicating the error that happened in the password handlers
1552TODO: these delayed error's should be replaced with Exceptions.
1553
1554returns undef if no error
1555
1556=cut
1557
1558sub passwordError {
1559 my ($this) = @_;
1560 return $this->{passwords}->error();
1561}
1562
1563=begin TML
1564
1565---++ ObjectMethod validateRegistrationField($field, $value ) -> $string
1566
1567This method is called for every field submitted during registration. It is also used
1568to validate the username when adding a member to a group.
1569
1570Returns a string containing the sanitized registration field, or can throw an Error::Simple
1571if the field contains illegal data to block the registration.
1572
1573returns the string unchanged if no issue found.
1574
1575=cut
1576
1577sub validateRegistrationField {
1578
1579 #my ($this, $field, $value) = @_;
1580 my $this = shift;
1581
1582# For now just let Foswiki::UserMapping do the validation - nothing special needed.
1583 return $this->SUPER::validateRegistrationField(@_);
1584}
1585
1586# TODO: and probably flawed in light of multiple cUIDs mapping to one wikiname
1587
# spent 21.1ms (9.76+11.4) within Foswiki::Users::TopicUserMapping::_cacheUser which was called 1069 times, avg 20µs/call: # 1069 times (9.76ms+11.4ms) by Foswiki::Users::TopicUserMapping::_loadMapping at line 1694, avg 20µs/call
sub _cacheUser {
15881069967µs my ( $this, $wikiname, $login ) = @_;
1589 ASSERT($wikiname) if DEBUG;
1590
15911069112µs $login ||= $wikiname;
1592
1593 #discard users that are the BaseUserMapper's responsibility
1594 return
159510691.45ms10692.59ms if ( $this->{session}->{users}->{basemapping}
# spent 2.59ms making 1069 calls to Foswiki::Users::BaseUserMapping::handlesUser, avg 2µs/call
1596 ->handlesUser( undef, $login, $wikiname ) );
1597
159810641.34ms10648.78ms my $cUID = $this->login2cUID( $login, 1 );
# spent 8.78ms making 1064 calls to Foswiki::Users::TopicUserMapping::login2cUID, avg 8µs/call
1599106491µs return unless ($cUID);
1600 ASSERT($cUID) if DEBUG;
1601
1602 #$this->{U2L}->{$cUID} = $login;
160310641.09ms $this->{U2W}->{$cUID} = $wikiname;
16041064572µs $this->{L2U}->{$login} = $cUID;
16051064859µs $this->{W2U}->{$wikiname} = $cUID;
1606
160710641.97ms return $cUID;
1608}
1609
1610# callback for search function to collate results
1611sub _collateGroups {
1612 my $ref = shift;
1613 my $group = shift;
1614 return unless $group;
1615 push( @{ $ref->{list} }, $group );
1616}
1617
1618# get a list of groups defined in this Wiki
1619sub _getListOfGroups {
1620 my $this = shift;
1621 my $reset = shift;
1622
1623 ASSERT( $this->isa('Foswiki::Users::TopicUserMapping') ) if DEBUG;
1624
1625 if ( !$this->{groupsList} || $reset ) {
1626 my $users = $this->{session}->{users};
1627 $this->{groupsList} = [];
1628
1629 #create a MetaCache _before_ we do silly things with the session's users
1630 $this->{session}->search->metacache();
1631
1632 # Temporarily set the user to admin, otherwise it cannot see groups
1633 # where %USERSWEB% is protected from view
1634 local $this->{session}->{user} = $Foswiki::cfg{SuperAdminGroup};
1635
1636 $this->{session}->search->searchWeb(
1637 _callback => \&_collateGroups,
1638 _cbdata => {
1639 list => $this->{groupsList},
1640 users => $users
1641 },
1642 web => $Foswiki::cfg{UsersWebName},
1643 topic => "*Group",
1644 scope => 'topic',
1645 search => '1',
1646 type => 'query',
1647 nosummary => 'on',
1648 nosearch => 'on',
1649 noheader => 'on',
1650 nototal => 'on',
1651 noempty => 'on',
1652 format => '$topic',
1653 separator => '',
1654 );
1655 }
1656 return $this->{groupsList};
1657}
1658
1659# Build hash to translate between username (e.g. jsmith)
1660# and WikiName (e.g. Main.JaneSmith).
1661# PRIVATE subclasses should *not* implement this.
1662
# spent 31.9ms (8.96+22.9) within Foswiki::Users::TopicUserMapping::_loadMapping which was called 5 times, avg 6.37ms/call: # 4 times (8.96ms+22.9ms) by Foswiki::Users::TopicUserMapping::_userReallyExists at line 228, avg 7.96ms/call # once (900ns+0s) by Foswiki::Users::TopicUserMapping::getWikiName at line 538
sub _loadMapping {
166351µs my $this = shift;
1664
1665510µs return if $this->{CACHED};
16661800ns $this->{CACHED} = 1;
1667
1668 #TODO: should only really do this mapping IF the user is in the password file.
1669 # except if we can't 'fetchUsers' like in the Passord='none' case -
1670 # in which case the only time we
1671 # know a login is real, is when they are logged in :(
167216µs if ( ( $Foswiki::cfg{Register}{AllowLoginName} )
1673 || ( !$this->{passwords}->canFetchUsers() ) )
1674 {
16751700ns my $session = $this->{session};
167613µs183µs if (
# spent 83µs making 1 call to Foswiki::topicExists
1677 $session->topicExists(
1678 $Foswiki::cfg{UsersWebName},
1679 $Foswiki::cfg{UsersTopicName}
1680 )
1681 )
1682 {
168314µs11.26ms my $usersTopicObject = Foswiki::Meta->load(
# spent 1.26ms making 1 call to Foswiki::Meta::load
1684 $session,
1685 $Foswiki::cfg{UsersWebName},
1686 $Foswiki::cfg{UsersTopicName}
1687 );
168812µs119µs my $text = $usersTopicObject->text() || '';
# spent 19µs making 1 call to Foswiki::Meta::text
1689
1690 # Get the WikiNames and userids, and build hashes in both directions
1691 # This matches:
1692 # * WikiGuest - guest - 10 Mar 2005
1693 # * WikiGuest - 10 Mar 2005
169410691.19ms106921.1ms $text =~
# spent 21.1ms making 1069 calls to Foswiki::Users::TopicUserMapping::_cacheUser, avg 20µs/call
169516.97ms6406µss/^\s*\* (?:$Foswiki::regex{webNameRegex}\.)?($Foswiki::regex{wikiWordRegex})\s*(?:-\s*(\S+)\s*)?-.*$/(_cacheUser( $this, $1, $2)||'')/gme;
# spent 406µs making 6 calls to utf8::SWASHNEW, avg 68µs/call
1696 }
1697 }
1698 else {
1699
1700 #loginnames _are_ WikiNames so ask the Password handler for list of users
1701 my $iter = $this->{passwords}->fetchUsers();
1702 while ( $iter->hasNext() ) {
1703 my $login = $iter->next();
1704 _cacheUser( $this, $login, $login );
1705 }
1706 }
1707}
1708
1709# Get a list of *canonical user ids* from a text string containing a
1710# list of user *wiki* names, *login* names, and *group ids*.
1711sub _expandUserList {
1712 my ( $this, $names, $expand ) = @_;
1713
1714 $expand = 1 unless ( defined $expand );
1715
1716 # print STDERR "_expandUserList called $names - expand $expand \n";
1717
1718 $names ||= '';
1719
1720 # comma delimited list of users or groups
1721 # i.e.: "%MAINWEB%.UserA, UserB, Main.UserC # something else"
1722 $names =~ s/(<[^>]*>)//g; # Remove HTML tags
1723
1724 my @l;
1725 foreach my $ident ( split( /[\,\s]+/, $names ) ) {
1726
1727 # Dump the web specifier if userweb
1728 $ident =~ s/^($Foswiki::cfg{UsersWebName}|%USERSWEB%|%MAINWEB%)\.//;
1729 next unless $ident;
1730 if ( $this->isGroup($ident) ) {
1731 if ( !$expand ) {
1732 push( @l, $ident );
1733 }
1734 else {
1735 my $it =
1736 $this->eachGroupMember( $ident, { expand => $expand } );
1737 while ( $it->hasNext() ) {
1738 push( @l, $it->next() );
1739 }
1740 }
1741 }
1742 else {
1743
1744 # Might be a wiki name (wiki names may map to several cUIDs)
1745 my %namelist =
1746 map { $_ => 1 }
1747 @{ $this->{session}->{users}->findUserByWikiName($ident) };
1748
1749 # If we were not successful in finding by WikiName we assumed it
1750 # may be a login name (login names map to a single cUID).
1751 # If user is unknown we return whatever was listed so we can
1752 # remove deleted or misspelled users
1753 unless (%namelist) {
1754 my $cUID = $this->{session}->{users}->getCanonicalUserID($ident)
1755 || $ident;
1756 $namelist{$cUID} = 1 if $cUID;
1757 }
1758 push( @l, keys %namelist );
1759 }
1760 }
1761 return \@l;
1762}
1763
176414µs1;
1765__END__