c350dfa4a80a873c6dfe0468e29059ef73a0ac9a
[platal.git] / classes / pluser.php
1 <?php
2 /***************************************************************************
3 * Copyright (C) 2003-2011 Polytechnique.org *
4 * http://opensource.polytechnique.org/ *
5 * *
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
10 * *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program; if not, write to the Free Software *
18 * Foundation, Inc., *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
21
22 /**
23 * PlUserNotFound is raised when a user id cannot be linked to a real user.
24 * The @p results give the list hruids (useful when several users are found).
25 */
26 class UserNotFoundException extends Exception
27 {
28 public function __construct($results = array())
29 {
30 $this->results = $results;
31 parent::__construct();
32 }
33 }
34
35 interface PlUserInterface
36 {
37 public static function _default_user_callback($login, $results);
38
39 /**
40 * Determines if the @p login is an email address, and an email address not
41 * served locally by plat/al.
42 */
43 public static function isForeignEmailAddress($email);
44 }
45
46 /**
47 * Represents an user of plat/al (without any further assumption), with a
48 * special focus on always-used properties (identification fields, display name,
49 * forlife/bestalias emails, ...).
50 * NOTE: each implementation of plat/al-code MUST subclass PlUser, and name it
51 * 'User'.
52 */
53 abstract class PlUser implements PlUserInterface
54 {
55 /**
56 * User data enumerations.
57 */
58 const GENDER_FEMALE = true;
59 const GENDER_MALE = false;
60 const FORMAT_HTML = "html";
61 const FORMAT_TEXT = "text";
62
63 /**
64 * User data storage.
65 * By convention, null means the information hasn't been fetched yet, and
66 * false means the information is not available.
67 */
68
69 // uid is internal user ID (potentially numeric), whereas hruid is a
70 // "human readable" unique ID
71 protected $uid = null;
72 protected $hruid = null;
73
74 // User main email aliases (forlife is the for-life email address, bestalias
75 // is user-chosen preferred email address, email might be any email available
76 // for the user).
77 protected $forlife = null;
78 protected $bestalias = null;
79 protected $email = null;
80
81 // Display name is user-chosen name to display (eg. in "Welcome
82 // <display name> !"), while full name is the official full name.
83 protected $display_name = null;
84 protected $full_name = null;
85
86 // Other important parameters used when sending emails.
87 protected $gender = null; // Acceptable values are GENDER_MALE and GENDER_FEMALE
88 protected $email_format = null; // Acceptable values are FORMAT_HTML and FORMAT_TEXT
89
90 // Permissions
91 protected $perms = null;
92 protected $perm_flags = null;
93
94 // Other properties are listed in this key-value hash map.
95 protected $data = array();
96
97 /**
98 * Constructs the PlUser object from an identifier (any identifier which is
99 * understood by getLogin() implementation).
100 *
101 * @param $login An user login.
102 * @param $values List of known user properties.
103 */
104 public function __construct($login, $values = array())
105 {
106 $this->fillFromArray($values);
107
108 // If the user id was not part of the known values, determines it from
109 // the login.
110 if (!$this->uid) {
111 $this->uid = $this->getLogin($login);
112 }
113
114 // Preloads main properties (assumes the loader will lazily get them
115 // from variables already set in the object).
116 $this->loadMainFields();
117 }
118
119 /**
120 * Get the canonical user id for the @p login.
121 *
122 * @param $login An user login.
123 * @return The canonical user id.
124 * @throws UserNotFoundException when login is not found.
125 */
126 abstract protected function getLogin($login);
127
128 /**
129 * Loads the main properties (hruid, forlife, bestalias, ...) from the
130 * database. Should return immediately when the properties are already
131 * available.
132 */
133 abstract protected function loadMainFields();
134
135 /**
136 * Accessors to the main properties, ie. those available as top level
137 * object variables.
138 */
139 public function id()
140 {
141 return $this->uid;
142 }
143
144 public function login()
145 {
146 return $this->hruid;
147 }
148
149 public function isMe($other)
150 {
151 if (!$other) {
152 return false;
153 } else if ($other instanceof PlUser) {
154 return $other->id() == $this->id();
155 } else {
156 return false;
157 }
158 }
159
160 public function bestEmail()
161 {
162 if (!empty($this->bestalias)) {
163 return $this->bestalias;
164 }
165 return $this->email;
166 }
167 public function forlifeEmail()
168 {
169 if (!empty($this->forlife)) {
170 return $this->forlife;
171 }
172 return $this->email;
173 }
174 public function forlifeEmailAlternate()
175 {
176 if (!empty($this->forlife_alternate)) {
177 return $this->forlife_alternate;
178 }
179 return $this->email;
180 }
181
182 public function displayName()
183 {
184 return $this->display_name;
185 }
186 public function fullName()
187 {
188 return $this->full_name;
189 }
190
191 abstract public function password();
192
193 // Fallback value is GENDER_MALE.
194 public function isFemale()
195 {
196 return $this->gender == self::GENDER_FEMALE;
197 }
198
199 // Fallback value is FORMAT_TEXT.
200 public function isEmailFormatHtml()
201 {
202 return $this->email_format == self::FORMAT_HTML;
203 }
204
205 /**
206 * Other properties are available directly through the $data array, or as
207 * standard object variables, using a getter.
208 */
209 public function data()
210 {
211 return $this->data;
212 }
213
214 public function __get($name)
215 {
216 if (property_exists($this, $name)) {
217 return $this->$name;
218 }
219
220 if (isset($this->data[$name])) {
221 return $this->data[$name];
222 }
223
224 return null;
225 }
226
227 public function __isset($name)
228 {
229 return property_exists($this, $name) || isset($this->data[$name]);
230 }
231
232 public function __unset($name)
233 {
234 if (property_exists($this, $name)) {
235 $this->$name = null;
236 } else {
237 unset($this->data[$name]);
238 }
239 }
240
241 /**
242 * Fills the object properties using the @p associative array; the intended
243 * user case is to fill the object using SQL obtained arrays.
244 *
245 * @param $values Key-value array of user properties.
246 */
247 protected function fillFromArray(array $values)
248 {
249 // Merge main properties with existing ones.
250 unset($values['data']);
251 foreach ($values as $key => $value) {
252 if (property_exists($this, $key) && !isset($this->$key)) {
253 $this->$key = $value;
254 }
255 }
256
257 // Merge all value into the $this->data placeholder.
258 $this->data = array_merge($this->data, $values);
259 }
260
261 /**
262 * Adds properties to the object; this method does not allow the caller to
263 * update core properties (id, ...).
264 *
265 * @param $values An associative array of non-core properties.
266 */
267 public function addProperties(array $values)
268 {
269 foreach ($values as $key => $value) {
270 if (!property_exists($this, $key)) {
271 $this->data[$key] = $value;
272 }
273 }
274 }
275
276
277 /**
278 * Build the permissions flags for the user.
279 */
280 abstract protected function buildPerms();
281
282 /**
283 * Check wether the user got the given permission combination.
284 */
285 public function checkPerms($perms)
286 {
287 if (is_null($this->perm_flags)) {
288 $this->buildPerms();
289 }
290 if (is_null($this->perm_flags)) {
291 return false;
292 }
293 return $this->perm_flags->hasFlagCombination($perms);
294 }
295
296
297 /**
298 * Returns a valid User object built from the @p id and optionnal @p values,
299 * or returns false and calls the callback if the @p id is not valid.
300 */
301 public static function get($login, $callback = false)
302 {
303 return User::getWithValues($login, array(), $callback);
304 }
305
306 public static function getWithValues($login, $values, $callback = false)
307 {
308 if (!$callback) {
309 $callback = array('User', '_default_user_callback');
310 }
311
312 try {
313 return new User($login, $values);
314 } catch (UserNotFoundException $e) {
315 return call_user_func($callback, $login, $e->results);
316 }
317 }
318
319 public static function getWithUID($uid, $callback = false)
320 {
321 return User::getWithValues(null, array('uid' => $uid), $callback);
322 }
323
324 // Same as above, but using the silent callback as default.
325 public static function getSilent($login)
326 {
327 return User::getWithValues($login, array(), array('User', '_silent_user_callback'));
328 }
329
330 public static function getSilentWithValues($login, $values)
331 {
332 return User::getWithValues($login, $values, array('User', '_silent_user_callback'));
333 }
334
335 public static function getSilentWithUID($uid)
336 {
337 return User::getWithValues(null, array('uid' => $uid), array('User', '_silent_user_callback'));
338 }
339
340 /**
341 * Retrieves User objects corresponding to the @p logins, and eventually
342 * extracts and returns the @p property. If @p strict mode is disabled, it
343 * also includes logins for which no forlife was found (but it still calls
344 * the callback for them).
345 * In all cases, email addresses which are not from the local domains are
346 * kept.
347 *
348 * @param $logins Array of user logins.
349 * @param $property Property to retrieve from the User objects.
350 * @param $strict Should unvalidated logins be returned as-is or discarded ?
351 * @param $callback Callback to call when a login is unknown to the system.
352 * @return Array of validated user forlife emails.
353 */
354 private static function getBulkUserProperties($logins, $property, $strict, $callback)
355 {
356 if (!is_array($logins)) {
357 if (strlen(trim($logins)) == 0) {
358 return null;
359 }
360 $logins = preg_split("/[; ,\r\n\|]+/", $logins);
361 }
362
363 if ($logins) {
364 $list = array();
365 foreach ($logins as $i => $login) {
366 $login = trim($login);
367 if (empty($login)) {
368 continue;
369 }
370
371 if (($user = User::get($login, $callback))) {
372 $list[$i] = $user->$property();
373 } else if (!$strict || (User::isForeignEmailAddress($login) && isvalid_email($login))) {
374 $list[$i] = $login;
375 }
376 }
377 return $list;
378 }
379 return null;
380 }
381
382 /**
383 * Returns hruid corresponding to the @p logins. See getBulkUserProperties()
384 * for details.
385 */
386 public static function getBulkHruid($logins, $callback = false)
387 {
388 return self::getBulkUserProperties($logins, 'login', true, $callback);
389 }
390
391 /**
392 * Returns forlife emails corresponding to the @p logins. See
393 * getBulkUserProperties() for details.
394 */
395 public static function getBulkForlifeEmails($logins, $strict = true, $callback = false)
396 {
397 return self::getBulkUserProperties($logins, 'forlifeEmail', $strict, $callback);
398 }
399
400 /**
401 * Predefined callbacks for the user lookup; they are called when a given
402 * login is found not to be associated with any valid user. Silent callback
403 * does nothing; default callback is supposed to display an error message,
404 * using the Platal::page() hook.
405 */
406 public static function _silent_user_callback($login, $results)
407 {
408 return;
409 }
410
411 private static function stripBadChars($text)
412 {
413 return str_replace(array(' ', "'"), array('-', ''),
414 strtolower(stripslashes(replace_accent(trim($text)))));
415 }
416
417 /** Creates a username from a first and last name
418 * @param $firstname User's firstname
419 * @param $lasttname User's lastname
420 * return STRING the corresponding username
421 */
422 public static function makeUserName($firstname, $lastname)
423 {
424 return self::stripBadChars($firstname) . '.' . self::stripBadChars($lastname);
425 }
426
427 /**
428 * Creates a user forlive identifier from:
429 * @param $firstname User's firstname
430 * @param $lasttname User's lastname
431 * @param $category User's promotion or type of account
432 */
433 public static function makeHrid($firstname, $lastname, $category)
434 {
435 $cat = self::stripBadChars($category);
436 if (!cat) {
437 Platal::page()->kill("$category is not a suitable category.");
438 }
439
440 return self::makeUserName($firstname, $lastname) . '.' . $cat;
441 }
442
443 /** Reformats the firstname so that all letters are in lower case,
444 * except the first letter of each part of the name.
445 */
446 public static function fixFirstnameCase($firstname)
447 {
448 $firstname = strtolower($firstname);
449 $pieces = explode('-', $firstname);
450
451 foreach ($pieces as $piece) {
452 $subpieces = explode("'", $piece);
453 $usubpieces = '';
454
455 foreach ($subpieces as $subpiece) {
456 $usubpieces[] = ucwords($subpiece);
457 }
458 $upieces[] = implode("'", $usubpieces);
459 }
460 return implode('-', $upieces);
461 }
462 }
463
464 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
465 ?>