Filename | /var/www/foswikidev/core/lib/Foswiki/Users/TopicUserMapping.pm |
Statements | Executed 14146 statements in 27.3ms |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
1069 | 1 | 1 | 9.76ms | 21.1ms | _cacheUser | Foswiki::Users::TopicUserMapping::
5 | 2 | 1 | 8.96ms | 31.9ms | _loadMapping | Foswiki::Users::TopicUserMapping::
1066 | 3 | 2 | 7.21ms | 8.81ms | login2cUID | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 1.09ms | 1.10ms | finish | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 746µs | 832µs | BEGIN@35 | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 614µs | 716µs | new | Foswiki::Users::TopicUserMapping::
62 | 1 | 1 | 165µs | 165µs | isGroup | Foswiki::Users::TopicUserMapping::
4 | 3 | 1 | 66µs | 31.9ms | _userReallyExists | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 15µs | 38µs | getLoginName | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 15µs | 19µs | BEGIN@204 | Foswiki::Users::TopicUserMapping::
3 | 1 | 1 | 15µs | 31.9ms | handlesUser | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 11µs | 14µs | BEGIN@209 | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 10µs | 15µs | BEGIN@32 | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 10µs | 10µs | BEGIN@28 | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 9µs | 35µs | BEGIN@33 | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 9µs | 10µs | getWikiName | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 9µs | 108µs | BEGIN@34 | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 8µs | 22µs | BEGIN@31 | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 4µs | 4µs | BEGIN@36 | Foswiki::Users::TopicUserMapping::
1 | 1 | 1 | 800ns | 800ns | supportsRegistration | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | __ANON__[:467] | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | __ANON__[:475] | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | __ANON__[:631] | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | __ANON__[:782] | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | _clearGroupCache | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | _collateGroups | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | _expandUserList | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | _getListOfGroups | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | _maintainUsersTopic | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | _writeGroupTopic | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | addUser | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | addUserToGroup | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | checkPassword | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | eachGroup | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | eachGroupMember | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | eachMembership | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | eachUser | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | findUserByEmail | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | findUserByWikiName | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | getEmails | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | groupAllowsChange | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | groupAllowsView | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | isAdmin | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | mapper_getEmails | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | mapper_setEmails | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | passwordError | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | removeUser | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | removeUserFromGroup | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | setEmails | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | setPassword | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | userExists | Foswiki::Users::TopicUserMapping::
0 | 0 | 0 | 0s | 0s | validateRegistrationField | Foswiki::Users::TopicUserMapping::
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 | |||||
7 | use | ||||
8 | |||||
9 | The User mapping is the process by which Foswiki maps from a username (a login name) | ||||
10 | to a wikiname and back. It is also where groups are defined. | ||||
11 | |||||
12 | By default Foswiki maintains user topics and group topics in the %MAINWEB% that | ||||
13 | define 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 | |||||
18 | Many sites will want to override this behaviour, for example to get users and groups from a corporate database. | ||||
19 | |||||
20 | This class implements the basic Foswiki behaviour using topics to store users, | ||||
21 | but is also designed to be subclassed so that other services can be used. | ||||
22 | |||||
23 | Subclasses should be named 'XxxxUserMapping' so that configure can find them. | ||||
24 | |||||
25 | =cut | ||||
26 | |||||
27 | package Foswiki::Users::TopicUserMapping; | ||||
28 | 2 | 43µs | 1 | 10µs | # spent 10µs within Foswiki::Users::TopicUserMapping::BEGIN@28 which was called:
# once (10µs+0s) by Foswiki::Users::new at line 28 # spent 10µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@28 |
29 | 1 | 7µs | our @ISA = ('Foswiki::UserMapping'); | ||
30 | |||||
31 | 2 | 29µs | 2 | 35µ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 # spent 22µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@31
# spent 13µs making 1 call to strict::import |
32 | 2 | 24µs | 2 | 19µ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 # spent 15µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@32
# spent 5µs making 1 call to warnings::import |
33 | 2 | 28µs | 2 | 61µ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 # spent 35µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@33
# spent 26µs making 1 call to Exporter::import |
34 | 2 | 28µs | 2 | 208µ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 # spent 108µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@34
# spent 100µs making 1 call to Error::import |
35 | 2 | 96µs | 1 | 832µ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 # spent 832µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@35 |
36 | 2 | 440µs | 1 | 4µs | # spent 4µs within Foswiki::Users::TopicUserMapping::BEGIN@36 which was called:
# once (4µs+0s) by Foswiki::Users::new at line 36 # 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 | |||||
45 | Constructs a new user mapping handler of this type, referring to $session | ||||
46 | for 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. | ||||
52 | 1 | 300ns | our $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 | ||||
57 | 1 | 900ns | my ( $class, $session ) = @_; | ||
58 | |||||
59 | 1 | 16µs | 1 | 9µs | my $this = $class->SUPER::new( $session, $FOSWIKI_USER_MAPPING_ID ); # spent 9µs making 1 call to Foswiki::UserMapping::new |
60 | |||||
61 | 1 | 2µs | my $implPasswordManager = $Foswiki::cfg{PasswordManager}; | ||
62 | 1 | 900ns | $implPasswordManager = 'Foswiki::Users::Password' | ||
63 | if ( $implPasswordManager eq 'none' ); | ||||
64 | 1 | 20µs | eval "require $implPasswordManager"; # spent 72µs executing statements in string eval | ||
65 | 1 | 200ns | die $@ if $@; | ||
66 | 1 | 4µs | 1 | 10µs | $this->{passwords} = $implPasswordManager->new($session); # spent 10µs making 1 call to Foswiki::Users::Password::new |
67 | |||||
68 | 1 | 2µs | 1 | 1µ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} = {}; | ||||
75 | 1 | 500ns | $this->{L2U} = {}; | ||
76 | 1 | 300ns | $this->{U2W} = {}; | ||
77 | 1 | 900ns | $this->{W2U} = {}; | ||
78 | 1 | 300ns | $this->{eachGroupMember} = {}; | ||
79 | 1 | 800ns | $this->{singleGroupMembers} = (); | ||
80 | |||||
81 | 1 | 4µs | return $this; | ||
82 | } | ||||
83 | |||||
84 | =begin TML | ||||
85 | |||||
86 | ---++ ObjectMethod finish() | ||||
87 | Break 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 | ||||
95 | 1 | 700ns | my $this = shift; | ||
96 | |||||
97 | 1 | 6µs | 1 | 7µs | $this->{passwords}->finish() if $this->{passwords}; # spent 7µs making 1 call to Foswiki::Users::Password::finish |
98 | 1 | 361µs | undef $this->{L2U}; | ||
99 | 1 | 381µs | undef $this->{U2W}; | ||
100 | 1 | 318µs | undef $this->{W2U}; | ||
101 | 1 | 9µs | undef $this->{passwords}; | ||
102 | 1 | 1µs | undef $this->{eachGroupMember}; | ||
103 | 1 | 600ns | undef $this->{singleGroupMembers}; | ||
104 | 1 | 17µs | 1 | 3µs | $this->SUPER::finish(); # spent 3µs making 1 call to Foswiki::UserMapping::finish |
105 | } | ||||
106 | |||||
107 | =begin TML | ||||
108 | |||||
109 | ---++ ObjectMethod supportsRegistration () -> false | ||||
110 | return 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 | ||||
115 | 1 | 5µs | return 1; | ||
116 | } | ||||
117 | |||||
118 | =begin TML | ||||
119 | |||||
120 | ---++ ObjectMethod handlesUser ( $cUID, $login, $wikiname) -> $boolean | ||||
121 | |||||
122 | Called by the Foswiki::Users object to determine which loaded mapping | ||||
123 | to use for a given user. | ||||
124 | |||||
125 | The user can be identified by any of $cUID, $login or $wikiname. Any of | ||||
126 | these parameters may be undef, and they should be tested in order; cUID | ||||
127 | first, then login, then wikiname. This mapping is special - for backwards | ||||
128 | compatibility, it assumes responsibility for _all_ non BaseMapping users. | ||||
129 | If you're needing to mix the TopicUserMapping with other mappings, | ||||
130 | define $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 | ||||
135 | 3 | 3µs | my ( $this, $cUID, $login, $wikiname ) = @_; | ||
136 | 3 | 6µ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 | ||||
144 | 1 | 600ns | return 1 if ( defined $cUID && $cUID =~ m/^($this->{mapping_id})/ ); | ||
145 | } | ||||
146 | |||||
147 | # Check the login id to see if we know it | ||||
148 | 1 | 7µs | 1 | 31.9ms | return 1 if ( $login && $this->_userReallyExists($login) ); # spent 31.9ms making 1 call to Foswiki::Users::TopicUserMapping::_userReallyExists |
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 | |||||
163 | Convert a login name to the corresponding canonical user name. The | ||||
164 | canonical name can be any string of 7-bit alphanumeric and underscore | ||||
165 | characters, 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. | ||||
169 | This 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 | ||||
174 | 1066 | 448µs | my ( $this, $login, $dontcheck ) = @_; | ||
175 | |||||
176 | 1066 | 111µs | 2 | 11µ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 | |||||
180 | 1066 | 2.84ms | 1066 | 1.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 | |||||
187 | Converts 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 | ||||
193 | 1 | 1µs | my ( $this, $cUID ) = @_; | ||
194 | ASSERT($cUID) if DEBUG; | ||||
195 | |||||
196 | 1 | 300ns | 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 | ||||
202 | 1 | 500ns | $login =~ s/$this->{mapping_id}// if $this->{mapping_id}; | ||
203 | |||||
204 | 2 | 59µs | 2 | 23µ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 # 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 | ||||
208 | 1 | 1µs | $login =~ s/_([0-9a-f][0-9a-f])/chr(hex($1))/gei; | ||
209 | 2 | 5.00ms | 2 | 17µ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 # spent 14µs making 1 call to Foswiki::Users::TopicUserMapping::BEGIN@209
# spent 3µs making 1 call to bytes::unimport |
210 | |||||
211 | 1 | 1µs | 1 | 5µs | return unless _userReallyExists( $this, $login ); # spent 5µs making 1 call to Foswiki::Users::TopicUserMapping::_userReallyExists |
212 | 1 | 2µs | 1 | 12µs | return unless ( $cUID eq $this->login2cUID($login) ); # spent 12µs making 1 call to Foswiki::Users::TopicUserMapping::login2cUID |
213 | |||||
214 | # Validated | ||||
215 | 1 | 5µs | 1 | 6µ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 | ||||
221 | 4 | 2µs | my ( $this, $login ) = @_; | ||
222 | |||||
223 | 4 | 3µs | if ( $Foswiki::cfg{Register}{AllowLoginName} | ||
224 | || $Foswiki::cfg{PasswordManager} eq 'none' ) | ||||
225 | { | ||||
226 | |||||
227 | # need to use the WikiUsers file | ||||
228 | 4 | 46µs | 4 | 31.9ms | $this->_loadMapping(); # spent 31.9ms making 4 calls to Foswiki::Users::TopicUserMapping::_loadMapping, avg 7.96ms/call |
229 | 4 | 16µ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 | |||||
254 | throws an Error::Simple | ||||
255 | |||||
256 | Add a user to the persistent mapping that maps from usernames to wikinames | ||||
257 | and 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). | ||||
260 | Names must be acceptable to $Foswiki::cfg{NameFilter} | ||||
261 | $login must *always* be specified. $wikiname may be undef, in which case | ||||
262 | the user mapper should make one up. | ||||
263 | This function must return a *canonical user id* that it uses to uniquely | ||||
264 | identify the user. This can be the login name, or the wikiname if they | ||||
265 | are all guaranteed unigue, or some other string consisting only of 7-bit | ||||
266 | alphanumerics and underscores. | ||||
267 | if 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 | |||||
273 | sub 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 | |||||
325 | throws an Error::Simple | ||||
326 | |||||
327 | Add or remove a user to/from the persistent mapping that maps from usernames to wikinames | ||||
328 | and vice-versa. The default implementation uses a special topic called | ||||
329 | "WikiUsers" in the users web. =cut | ||||
330 | |||||
331 | =cut | ||||
332 | |||||
333 | sub _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 =~ | ||||
399 | m/^\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 | |||||
484 | Delete the users entry. Removes the user from the password | ||||
485 | manager and user mapping manager. Does *not* remove their personal | ||||
486 | topics, which may still be linked. | ||||
487 | |||||
488 | Note that this must be called with the cUID. If any doubt, resolve the cUID | ||||
489 | by $this->{session}->{users}->getCanonicalUserID($identity). | ||||
490 | |||||
491 | =cut | ||||
492 | |||||
493 | sub 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 | |||||
523 | Map a canonical user name to a wikiname. If it fails to find a | ||||
524 | WikiName, it will attempt to find a matching loginname, and use | ||||
525 | an escaped version of that. | ||||
526 | If 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 | ||||
531 | 1 | 1µs | my ( $this, $cUID ) = @_; | ||
532 | ASSERT($cUID) if DEBUG; | ||||
533 | 1 | 100ns | ASSERT( $cUID =~ m/^$this->{mapping_id}/ ) if DEBUG; | ||
534 | |||||
535 | 1 | 300ns | my $wikiname; | ||
536 | |||||
537 | 1 | 1µs | if ( $Foswiki::cfg{Register}{AllowLoginName} ) { | ||
538 | 1 | 800ns | 1 | 900ns | $this->_loadMapping(); # spent 900ns making 1 call to Foswiki::Users::TopicUserMapping::_loadMapping |
539 | 1 | 900ns | $wikiname = $this->{U2W}->{$cUID}; | ||
540 | } | ||||
541 | else { | ||||
542 | |||||
543 | # If the mapping isn't enabled there's no point in loading it | ||||
544 | } | ||||
545 | |||||
546 | 1 | 300ns | 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 | |||||
555 | 1 | 4µs | return $wikiname; | ||
556 | } | ||||
557 | |||||
558 | =begin TML | ||||
559 | |||||
560 | ---++ ObjectMethod userExists($cUID) -> $boolean | ||||
561 | |||||
562 | Determine if the user already exists or not. Whether a user exists | ||||
563 | or not is determined by the password manager. | ||||
564 | |||||
565 | =cut | ||||
566 | |||||
567 | sub 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 | |||||
612 | See baseclass for documentation | ||||
613 | |||||
614 | =cut | ||||
615 | |||||
616 | sub 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 | |||||
639 | See baseclass for documentation | ||||
640 | |||||
641 | =cut | ||||
642 | |||||
643 | 1 | 200ns | my %expanding; # Prevents loops in nested groups | ||
644 | |||||
645 | sub 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 | |||||
727 | See 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 | ||||
732 | 62 | 25µs | my ( $this, $user ) = @_; | ||
733 | |||||
734 | # Groups have the same username as wikiname as canonical name | ||||
735 | 62 | 31µs | return 1 if $user eq $Foswiki::cfg{SuperAdminGroup}; | ||
736 | |||||
737 | 62 | 163µ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 | |||||
757 | See baseclass for documentation | ||||
758 | |||||
759 | =cut | ||||
760 | |||||
761 | sub 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 | |||||
771 | See baseclass for documentation | ||||
772 | |||||
773 | =cut | ||||
774 | |||||
775 | sub 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 | |||||
790 | returns 1 if the group is able to be viewed by the current logged in user | ||||
791 | |||||
792 | implemented using topic VIEW permissions | ||||
793 | |||||
794 | =cut | ||||
795 | |||||
796 | sub 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 | |||||
823 | returns 1 if the group is able to be modified by $cuid | ||||
824 | |||||
825 | implemented using topic CHANGE permissions | ||||
826 | |||||
827 | =cut | ||||
828 | |||||
829 | sub 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 | ||||
858 | adds the user specified by the cuid to the group. | ||||
859 | If the group does not exist, it will return false and do nothing, unless the create flag is set. | ||||
860 | |||||
861 | cuid be a groupname which is added like it was an unknown user | ||||
862 | |||||
863 | =cut | ||||
864 | |||||
865 | sub 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. | ||||
982 | sub _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 | |||||
1074 | sub 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 | |||||
1161 | Removes the cache entries for unexpanded and expanded groups, | ||||
1162 | and searches un-expanded groups for any nesting group references | ||||
1163 | clearing them as well. | ||||
1164 | |||||
1165 | Note: This is not recursive and does not attempt to handle | ||||
1166 | more than one level of nested groups. | ||||
1167 | |||||
1168 | =cut | ||||
1169 | |||||
1170 | sub _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 | |||||
1191 | True if the user is an admin | ||||
1192 | * is $Foswiki::cfg{SuperAdminGroup} | ||||
1193 | * is a member of the $Foswiki::cfg{SuperAdminGroup} | ||||
1194 | |||||
1195 | =cut | ||||
1196 | |||||
1197 | sub 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 | ||||
1217 | Return a list of canonical user names for the users that have this email | ||||
1218 | registered with the password manager or the user mapping manager. | ||||
1219 | |||||
1220 | The password manager is asked first for whether it maps emails. | ||||
1221 | If it doesn't, then the user mapping manager is asked instead. | ||||
1222 | |||||
1223 | =cut | ||||
1224 | |||||
1225 | sub 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 | |||||
1263 | If $name is a user, return their email addresses. If it is a group, | ||||
1264 | return the addresses of everyone in the group. | ||||
1265 | |||||
1266 | The password manager and user mapping manager are both consulted for emails | ||||
1267 | for each user (where they are actually found is implementation defined). | ||||
1268 | |||||
1269 | Duplicates are removed from the list. | ||||
1270 | |||||
1271 | =cut | ||||
1272 | |||||
1273 | sub 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 | |||||
1323 | Set the email address(es) for the given user. | ||||
1324 | The password manager is tried first, and if it doesn't want to know the | ||||
1325 | user mapping manager is tried. | ||||
1326 | |||||
1327 | =cut | ||||
1328 | |||||
1329 | sub 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 | |||||
1347 | Only used if passwordManager->isManagingEmails= = =false or | ||||
1348 | $Foswiki::cfg{TopicUserMapping}{ForceManageEmails} is enabled. | ||||
1349 | (The emails are stored in the user topics. | ||||
1350 | |||||
1351 | Note: This method is PUBLIC because it is used by the tools/upgrade_emails.pl | ||||
1352 | script, which needs to kick down to the mapper to retrieve email addresses | ||||
1353 | from Wiki topics. | ||||
1354 | |||||
1355 | =cut | ||||
1356 | |||||
1357 | sub 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 | |||||
1392 | Only used if =passwordManager->isManagingEmails= = =false= | ||||
1393 | or $Foswiki::cfg{TopicUserMapping}{ForceManageEmails} is enabled. | ||||
1394 | (emails are stored in user topics | ||||
1395 | |||||
1396 | =cut | ||||
1397 | |||||
1398 | sub 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 | |||||
1439 | See baseclass for documentation | ||||
1440 | |||||
1441 | The $skipExistanceCheck parameter | ||||
1442 | is private to this module, and blocks the standard existence check | ||||
1443 | to avoid reading .htpasswd when checking group memberships). | ||||
1444 | |||||
1445 | =cut | ||||
1446 | |||||
1447 | sub 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 | |||||
1499 | Finds if the password is valid for the given user. | ||||
1500 | |||||
1501 | Returns 1 on success, undef on failure. | ||||
1502 | |||||
1503 | =cut | ||||
1504 | |||||
1505 | sub 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 | |||||
1521 | BEWARE: $user should be a cUID, but is a login when the resetPassword | ||||
1522 | functionality is used. | ||||
1523 | The UserMapper needs to convert either one to a valid login for use by | ||||
1524 | the Password manager | ||||
1525 | |||||
1526 | TODO: needs fixing | ||||
1527 | |||||
1528 | If the $oldPassU matches matches the user's password, then it will | ||||
1529 | replace it with $newPassU. | ||||
1530 | |||||
1531 | If $oldPassU is not correct and not 1, will return 0. | ||||
1532 | |||||
1533 | If $oldPassU is 1, will force the change irrespective of | ||||
1534 | the existing password, adding the user if necessary. | ||||
1535 | |||||
1536 | Otherwise returns 1 on success, undef on failure. | ||||
1537 | |||||
1538 | =cut | ||||
1539 | |||||
1540 | sub 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 | |||||
1551 | returns a string indicating the error that happened in the password handlers | ||||
1552 | TODO: these delayed error's should be replaced with Exceptions. | ||||
1553 | |||||
1554 | returns undef if no error | ||||
1555 | |||||
1556 | =cut | ||||
1557 | |||||
1558 | sub passwordError { | ||||
1559 | my ($this) = @_; | ||||
1560 | return $this->{passwords}->error(); | ||||
1561 | } | ||||
1562 | |||||
1563 | =begin TML | ||||
1564 | |||||
1565 | ---++ ObjectMethod validateRegistrationField($field, $value ) -> $string | ||||
1566 | |||||
1567 | This method is called for every field submitted during registration. It is also used | ||||
1568 | to validate the username when adding a member to a group. | ||||
1569 | |||||
1570 | Returns a string containing the sanitized registration field, or can throw an Error::Simple | ||||
1571 | if the field contains illegal data to block the registration. | ||||
1572 | |||||
1573 | returns the string unchanged if no issue found. | ||||
1574 | |||||
1575 | =cut | ||||
1576 | |||||
1577 | sub 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 | ||||
1588 | 1069 | 967µs | my ( $this, $wikiname, $login ) = @_; | ||
1589 | ASSERT($wikiname) if DEBUG; | ||||
1590 | |||||
1591 | 1069 | 112µs | $login ||= $wikiname; | ||
1592 | |||||
1593 | #discard users that are the BaseUserMapper's responsibility | ||||
1594 | return | ||||
1595 | 1069 | 1.45ms | 1069 | 2.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 | |||||
1598 | 1064 | 1.34ms | 1064 | 8.78ms | my $cUID = $this->login2cUID( $login, 1 ); # spent 8.78ms making 1064 calls to Foswiki::Users::TopicUserMapping::login2cUID, avg 8µs/call |
1599 | 1064 | 91µs | return unless ($cUID); | ||
1600 | ASSERT($cUID) if DEBUG; | ||||
1601 | |||||
1602 | #$this->{U2L}->{$cUID} = $login; | ||||
1603 | 1064 | 1.09ms | $this->{U2W}->{$cUID} = $wikiname; | ||
1604 | 1064 | 572µs | $this->{L2U}->{$login} = $cUID; | ||
1605 | 1064 | 859µs | $this->{W2U}->{$wikiname} = $cUID; | ||
1606 | |||||
1607 | 1064 | 1.97ms | return $cUID; | ||
1608 | } | ||||
1609 | |||||
1610 | # callback for search function to collate results | ||||
1611 | sub _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 | ||||
1619 | sub _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 | ||||
1663 | 5 | 1µs | my $this = shift; | ||
1664 | |||||
1665 | 5 | 10µs | return if $this->{CACHED}; | ||
1666 | 1 | 800ns | $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 :( | ||||
1672 | 1 | 6µs | if ( ( $Foswiki::cfg{Register}{AllowLoginName} ) | ||
1673 | || ( !$this->{passwords}->canFetchUsers() ) ) | ||||
1674 | { | ||||
1675 | 1 | 700ns | my $session = $this->{session}; | ||
1676 | 1 | 3µs | 1 | 83µ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 | { | ||||
1683 | 1 | 4µs | 1 | 1.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 | ); | ||||
1688 | 1 | 2µs | 1 | 19µ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 | ||||
1694 | 1069 | 1.19ms | 1069 | 21.1ms | $text =~ # spent 21.1ms making 1069 calls to Foswiki::Users::TopicUserMapping::_cacheUser, avg 20µs/call |
1695 | 1 | 6.97ms | 6 | 406µs | s/^\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*. | ||||
1711 | sub _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 | |||||
1764 | 1 | 4µs | 1; | ||
1765 | __END__ |