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