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